diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml deleted file mode 100644 index e02c34fd8868e..0000000000000 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - <description value="Checks to see if the tracking script is in the dom of admin and if setting is turned to no it checks if the tracking script in the dom was removed"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-18192"/> - <group value="backend"/> - <group value="login"/> - </annotations> - - <!-- Logging in Magento admin --> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - </test> -</tests> \ No newline at end of file diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml index e6782dca897d7..f9d3c49d509e9 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml @@ -17,11 +17,11 @@ <element name="widgets" type="button" selector="#nav li[data-ui-id='menu-magento-widget-cms-widget-instance']"/> <element name="stores" type="button" selector="#menu-magento-backend-stores"/> <element name="configuration" type="button" selector="#nav li[data-ui-id='menu-magento-config-system-config']"/> - <element name="dashboard" type="button" selector="//li[@id='menu-magento-backend-dashboard']"/> - <element name="sales" type="button" selector="//li[@id='menu-magento-sales-sales']"/> - <element name="marketing" type="button" selector="//li[@id='menu-magento-backend-marketing']"/> - <element name="system" type="button" selector="//li[@id='menu-magento-backend-system']"/> - <element name="findPartners" type="button" selector="//li[@id='menu-magento-marketplace-partners']"/> + <element name="dashboard" type="button" selector="#menu-magento-backend-dashboard"/> + <element name="sales" type="button" selector="#menu-magento-sales-sales"/> + <element name="marketing" type="button" selector="#menu-magento-backend-marketing"/> + <element name="system" type="button" selector="#menu-magento-backend-system"/> + <element name="findPartners" type="button" selector="#menu-magento-marketplace-partners"/> <!-- Navigate menu selectors --> <element name="menuItem" type="button" selector="li[data-ui-id='menu-{{dataUiId}}']" parameterized="true" timeout="30"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml new file mode 100644 index 0000000000000..a37bb443224b4 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontBundleValidationMessageActionGroup"> + <annotations> + <description>Check error message in validation message box</description> + </annotations> + <arguments> + <argument name="message" type="string"/> + </arguments> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{StorefrontBundledSection.validationMessageBox}}" userInput="{{message}}" stepKey="seeErrorHoldMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml new file mode 100644 index 0000000000000..35ac68b602a5e --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontBundleValidationMessagesCountActionGroup"> + <annotations> + <description>Check if there's a validation message box on page and asserts the validation messages number</description> + </annotations> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{StorefrontBundledSection.validationMessageBox}}" stepKey="seeErrorBox"/> + <seeNumberOfElements selector="{{StorefrontBundledSection.validationMessageBox}}" userInput="1" stepKey="seeOneErrorBox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml new file mode 100644 index 0000000000000..f0afcffca816c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAddToTheCartButtonActionGroup"> + <annotations> + <description>Clicks 'Add to Cart' on a Storefront Bundled Product page.</description> + </annotations> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="waitForAddToCartButton"/> + <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickOnAddToCartButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml index 7a188fd58e1af..739c2839e990d 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml @@ -14,8 +14,8 @@ <element name="bundleOptionSelection" type="checkbox" selector="//div[@class='nested options-list']/div[{{optionNumber}}]/label[@class='label']" parameterized="true"/> <!--Description--> <!--CE exclusively--> - <element name="longDescriptionText" type="text" selector="//*[@id='description']/div/div" timeout="30"/> - <element name="shortDescriptionText" type="text" selector="//div[@class='product attribute overview']" timeout="30"/> + <element name="longDescriptionText" type="text" selector="#description>div>div" timeout="30"/> + <element name="shortDescriptionText" type="text" selector="div.product.attribute.overview" timeout="30"/> <!--NameOfProductOnProductPage--> <element name="bundleProductName" type="text" selector="//*[@id='maincontent']//span[@itemprop='name']"/> <!--PageNotFoundErrorMessage--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml index c47cf6095c777..1dea8958c3552 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml @@ -17,7 +17,7 @@ <element name="updateCart" type="button" selector="#product-updatecart-button" timeout="30"/> <element name="configuredPrice" type="block" selector=".price-configured_price .price"/> <element name="fixedPricing" type="text" selector="//div[@class='price-box price-final_price']//span[@id]//..//span[contains(text(),'{{var1}}')]" parameterized="true"/> - <element name="customizeProduct" type="button" selector="//*[@id='bundle-slide']"/> + <element name="customizeProduct" type="button" selector="#bundle-slide"/> <element name="customizableBundleItemOption" type="text" selector="//div[@class='field choice'][1]//input[@type='checkbox']"/> <element name="customizableBundleItemOption2" type="text" selector="//div[@class='field choice'][2]//input[@type='checkbox']"/> <element name="nthOptionDiv" type="block" selector="#product-options-wrapper div.field.option:nth-of-type({{var}})" parameterized="true"/> @@ -38,5 +38,6 @@ <element name="currencyTrigger" type="select" selector="#switcher-currency-trigger" timeout="30"/> <element name="currency" type="select" selector="//a[text()='{{arg}}']" parameterized="true"/> <element name="multiSelectOption" type="select" selector="//div[@class='field option required']//select"/> + <element name="validationMessageBox" type="block" selector="#validation-message-box"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml new file mode 100644 index 0000000000000..91cc58ee0119b --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontBundleCheckBoxOptionValidationTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product validation before add to cart"/> + <title value="Customer should be able to see only one validation message for checkbox option group"/> + <description value="Customer should be able to see only one validation message for checkbox option group"/> + <testCaseId value="MC-35133"/> + <severity value="MINOR"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simpleProduct1" before="bundleProduct"/> + <createData entity="ApiProductWithDescription" stepKey="simpleProduct2" after="simpleProduct1"/> + <createData entity="ApiBundleProduct" stepKey="bundleProduct"/> + <createData entity="CheckboxOption" stepKey="checkboxBundleOption"> + <requiredEntity createDataKey="bundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + <field key="qty">2</field> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + <field key="qty">4</field> + </createData> + <magentoCron stepKey="runCronIndex" groups="index"/> + </before> + <after> + <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductStorefront"> + <argument name="productUrl" value="$$bundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="customizeBundleProduct"/> + <actionGroup ref="StorefrontAddToTheCartButtonActionGroup" stepKey="addToCartBundleProduct"/> + <actionGroup ref="AssertStorefrontBundleValidationMessagesCountActionGroup" stepKey="assertBundleValidationCount"/> + <actionGroup ref="AssertStorefrontBundleValidationMessageActionGroup" stepKey="assertBundleValidationMessage"> + <argument name="message" value="Please select one of the options."/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml index 5b56598dc58e2..4ba6fd6183653 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml @@ -8,40 +8,55 @@ <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox */ ?> <?php $_option = $block->getOption() ?> <?php $_selections = $_option->getSelections() ?> +<?php $inputClass = 'checkbox product bundle option bundle-option-' . $block->escapeHtmlAttr($_option->getId()) ?> +<?php $inputId = 'bundle-option-' . $block->escapeHtmlAttr($_option->getId()) ?> +<?php $inputName = 'bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']' ?> +<?php $dataValidation = 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . + $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"' ?> + <div class="field option <?= ($_option->getRequired()) ? ' required': '' ?>"> <label class="label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> <div class="control"> <div class="nested options-list"> - <?php if ($block->showSingle()) : ?> + <?php if ($block->showSingle()): ?> <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> product bundle option" name="bundle_option[<?= $block->escapeHtml($_option->getId()) ?>]" value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> - <?php else :?> - <?php foreach ($_selections as $_selection) : ?> + <?php else: ?> + <?php foreach ($_selections as $selection): ?> + <?php $sectionId = $selection->getSelectionId() ?> <div class="field choice"> - <input class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> checkbox product bundle option change-container-classname" - id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + <input class="<?=/* @noEscape */ $inputClass ?> change-container-classname" + id="<?=/* @noEscape */ $inputId . '-' . $block->escapeHtmlAttr($sectionId)?>" type="checkbox" - <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"'; } ?> - name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" - data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" - <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> - <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> - value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> + <?php if ($_option->getRequired()): ?> + <?= /* @noEscape */ $dataValidation ?> + <?php endif;?> + name="<?=/* @noEscape */ $inputName .'['. $block->escapeHtmlAttr($sectionId)?>]" + data-selector="<?= /* @noEscape */ $inputName.'['.$block->escapeHtmlAttr($sectionId)?>]" + <?php if ($block->isSelected($selection)): ?> + <?= ' checked="checked"' ?> + <?php endif; ?> + <?php if (!$selection->isSaleable()): ?> + <?= ' disabled="disabled"' ?> + <?php endif; ?> + value="<?= $block->escapeHtmlAttr($sectionId) ?>" + data-errors-message-box="#validation-message-box"/> <label class="label" - for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> - <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + for="<?= /* @noEscape */ $inputId . '-' . $block->escapeHtmlAttr($sectionId) ?>"> + <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($selection) ?></span> <br/> - <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($selection) ?> </label> </div> <?php endforeach; ?> <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> + <div id="validation-message-box"></div> <?php endif; ?> </div> </div> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml index 9103c4191544c..030c9f5efcf50 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml @@ -14,7 +14,7 @@ <element name="customer" type="button" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='Customers']"/> <element name="customerConfig" type="text" selector="//span[text()='Customer Configuration']"/> <element name="captcha" type="button" selector="#customer_captcha-head"/> - <element name="dependent" type="button" selector="//a[@id='customer_captcha-head' and @class='open']"/> + <element name="dependent" type="button" selector="a#customer_captcha-head.open"/> <element name="forms" type="multiselect" selector="#customer_captcha_forms"/> <element name="createUser" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_create']"/> <element name="forgotpassword" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_forgotpassword']"/> diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index cf12e332be86d..8da2614eb2ced 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -261,6 +261,10 @@ public function execute() unset($data['apply_to']); } + if ($model->getBackendType() == 'static' && !$model->getIsUserDefined()) { + $data['frontend_class'] = $model->getFrontendClass(); + } + $model->addData($data); if (!$attributeId) { diff --git a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php index 497ed2fd49953..a928ddea03a70 100644 --- a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php +++ b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php @@ -8,21 +8,21 @@ namespace Magento\Catalog\Model; /** - * Filter custom attributes for product using the blacklist + * Filter custom attributes for product using the excluded list */ class FilterProductCustomAttribute { /** * @var array */ - private $blackList; + private $excludedList; /** - * @param array $blackList + * @param array $excludedList */ - public function __construct(array $blackList = []) + public function __construct(array $excludedList = []) { - $this->blackList = $blackList; + $this->excludedList = $excludedList; } /** @@ -33,6 +33,6 @@ public function __construct(array $blackList = []) */ public function execute(array $attributes): array { - return array_diff_key($attributes, array_flip($this->blackList)); + return array_diff_key($attributes, array_flip($this->excludedList)); } } diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php index 77dedb9eb0121..3494fd00a8b6c 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php @@ -72,6 +72,8 @@ public function renderRangeLabel($fromPrice, $toPrice) } /** + * Prepare range data + * * @param int $range * @param int[] $dbRanges * @return array @@ -81,12 +83,10 @@ public function renderRangeData($range, $dbRanges) if (empty($dbRanges)) { return []; } - $lastIndex = array_keys($dbRanges); - $lastIndex = $lastIndex[count($lastIndex) - 1]; foreach ($dbRanges as $index => $count) { - $fromPrice = $index == 1 ? '' : ($index - 1) * $range; - $toPrice = $index == $lastIndex ? '' : $index * $range; + $fromPrice = $index == 1 ? 0 : ($index - 1) * $range; + $toPrice = $index * $range; $this->itemDataBuilder->addItemData( $this->renderRangeLabel($fromPrice, $toPrice), $fromPrice . '-' . $toPrice, diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php index 3d4d9f607da48..40fe6a01e260c 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php +++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php @@ -83,6 +83,12 @@ public function getFailedItems() } } + /** + * Clear validation messages to prevent wrong validation for subsequent price update. + * Work around for backward compatible changes. + */ + $this->failedItems = []; + return $failedItems; } } diff --git a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php deleted file mode 100644 index 404760a51eff5..0000000000000 --- a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Model\Product; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Store\Model\StoreManagerInterface; - -/** - * Class to check that product is saleable. - */ -class SalabilityChecker -{ - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * @param ProductRepositoryInterface $productRepository - * @param StoreManagerInterface $storeManager - */ - public function __construct( - ProductRepositoryInterface $productRepository, - StoreManagerInterface $storeManager - ) { - $this->productRepository = $productRepository; - $this->storeManager = $storeManager; - } - - /** - * Check if product is salable. - * - * @param int|string $productId - * @param int|null $storeId - * @return bool - */ - public function isSalable($productId, $storeId = null): bool - { - if ($storeId === null) { - $storeId = $this->storeManager->getStore()->getId(); - } - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->productRepository->getById($productId, false, $storeId); - - return $product->isSalable(); - } -} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php index fab2441db26c9..939f9d354af85 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php @@ -8,11 +8,15 @@ use Magento\Catalog\Model\Category; /** + * Aggregate count for parent category after deleting child category + * * Class AggregateCount */ class AggregateCount { /** + * Reduces children count for parent categories + * * @param Category $category * @return void */ @@ -25,9 +29,7 @@ public function processDelete(Category $category) */ $parentIds = $category->getParentIds(); if ($parentIds) { - $childDecrease = $category->getChildrenCount() + 1; - // +1 is itself - $data = ['children_count' => new \Zend_Db_Expr('children_count - ' . $childDecrease)]; + $data = ['children_count' => new \Zend_Db_Expr('children_count - 1')]; $where = ['entity_id IN(?)' => $parentIds]; $resourceModel->getConnection()->update($resourceModel->getEntityTable(), $data, $where); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php index cf5760b0c33a9..8d03eb3ccafc9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php @@ -147,7 +147,7 @@ private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositi * @param bool $insert * @return array */ - private function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false) + public function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false) { if (empty($insertLinks)) { return []; diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml new file mode 100644 index 0000000000000..020fb27063be7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminChangeCategoryNameActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string" defaultValue="{{_defaultCategory.name}}"/> + </arguments> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="updateCategoryName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml new file mode 100644 index 0000000000000..14a7967422332 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.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="AdminChangeCategoryNameOnStoreViewLevelActionGroup"> + <annotations> + <description>Updates the Category Name for proper Store View.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="changeNameField"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml new file mode 100644 index 0000000000000..bd7eb664819dd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.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="AdminEnableCategoryActionGroup"> + <annotations> + <description>Enable the category</description> + </annotations> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="enableCategory"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml new file mode 100644 index 0000000000000..8ecef0df400be --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetManageStockConfigActionGroup"> + <annotations> + <description>Set "Manage Stock" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="{{value}}" + stepKey="setManageStockConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml new file mode 100644 index 0000000000000..0f6a8df1ebf8c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetMaxAllowedQtyForProductActionGroup"> + <annotations> + <description>Fills in the "Maximum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="{{qty}}" + stepKey="fillMaxAllowedQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml new file mode 100644 index 0000000000000..abbfdacc15395 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetMinAllowedQtyForProductActionGroup"> + <annotations> + <description>Fills in the "Minimum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="{{qty}}" + stepKey="fillMinAllowedQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml new file mode 100644 index 0000000000000..4ecfa0762db9f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.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="AdminSetNotifyBelowQtyValueActionGroup"> + <annotations> + <description>Fills in the "Notify for Quantity Below" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" + stepKey="uncheckNotifyBelowQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="{{qty}}" + stepKey="fillNotifyBelowQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml new file mode 100644 index 0000000000000..7846689a8d643 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.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="AdminSetQtyUsesDecimalsConfigActionGroup"> + <annotations> + <description>Set "Qty Uses Decimals" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + </arguments> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="{{value}}" + stepKey="setQtyUsesDecimalsConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml new file mode 100644 index 0000000000000..98156eb1ad9b1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.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="AdminSetStockStatusConfigActionGroup"> + <annotations> + <description>Set "Stock status" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="stockStatus" type="string"/> + </arguments> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" + userInput="{{stockStatus}}" stepKey="selectStockStatus"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml new file mode 100644 index 0000000000000..3a75b0a3cd361 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.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="AssertAdminCategoryIsListedInCategoriesTreeActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="seeCategoryInTree"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml new file mode 100644 index 0000000000000..e0a98a8932d4d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.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="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="doNotSeeCategoryInTree"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml new file mode 100644 index 0000000000000..cead98091d268 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup"> + <annotations> + <description>Validate that the Category is not present in menu on Frontend.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" + stepKey="doNotSeeCatergoryInStoreFront"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml new file mode 100644 index 0000000000000..c56a18b4895a4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertCategoryNameIsShownInMenuActionGroup"> + <annotations> + <description>Validate that the Category is present in menu on Frontend.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" + stepKey="seeCatergoryInStoreFront"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml new file mode 100644 index 0000000000000..65858be673dfa --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml @@ -0,0 +1,14 @@ +<?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="StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup"> + <dontSee userInput="Add to Wish List" selector="{{StorefrontProductPageSection.addToWishlist}}" stepKey="dontSeeElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml new file mode 100644 index 0000000000000..5b7dd3026a905 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.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"> + <actionGroup name="StorefrontClickOnProductFromSidebarCompareListActionGroup"> + <annotations> + <description>Click on the product item from the sidebar comparing list.</description> + </annotations> + + <arguments> + <argument name="product" type="entity"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontComparisonSidebarSection.ProductTitleByName((product.name)}}" stepKey="waitForAddedCompareProduct"/> + <click selector="{{StorefrontComparisonSidebarSection.ProductTitleByName((product.name))}}" stepKey="clickOnProductLink"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml new file mode 100644 index 0000000000000..4a403364a91e3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.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="StorefrontSwitchStoreActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="storeName" type="string"/> + </arguments> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickOnSwitchStoreButton"/> + <click selector="{{StorefrontFooterSection.storeLink(storeName)}}" stepKey="selectStoreToSwitchOn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml index c9b67e0db4398..1d6bb970ea4d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml @@ -30,4 +30,9 @@ <data key="label">No</data> <data key="value">0</data> </entity> + <entity name="CatalogInventoryOptionsOnlyXleftThreshold"> + <!-- Magento default value --> + <data key="path">cataloginventory/options/stock_threshold_qty</data> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml index 26946692ce050..7a829a5475758 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml @@ -8,7 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ProductDescriptionWYSIWYGToolbarSection"> - <element name="TinyMCE4" type="button" selector="//div[@id='editorproduct_form_description']//*[contains(@class,'mce-branding')]"/> + <element name="TinyMCE4" type="button" selector="div#editorproduct_form_description .mce-branding"/> <element name="showHideBtn" type="button" selector="#toggleproduct_form_description"/> <element name="InsertImageBtn" type="button" selector="#buttonsproduct_form_description > .scalable.action-add-image.plugin"/> <element name="Style" type="button" selector="//div[@id='editorproduct_form_description']//span[text()='Paragraph']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml index 544bdf85681c9..b919cdff2bb92 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml @@ -8,12 +8,12 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ProductWYSIWYGSection"> - <element name="Switcher" type="button" selector="//select[@id='dropdown-switcher']"/> + <element name="Switcher" type="button" selector="select#dropdown-switcher"/> <element name="v436" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 4.3.6']"/> <element name="v3" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 3.6(Deprecated)']"/> <element name="TinymceDescription3" type="button" selector="//span[text()='Description']"/> <element name="SaveConfig" type="button" selector="#save"/> <element name="v4" type="button" selector="#category_form_description_v4"/> - <element name="WYSIWYGBtn" type="button" selector=".//button[@class='action-default scalable action-wysiwyg']"/> + <element name="WYSIWYGBtn" type="button" selector="button.action-default.scalable.action-wysiwyg"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml index 78818dd37a5d4..7be02126e3a0f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -25,5 +25,6 @@ <element name="customOptionDropDown" type="select" selector="//*[@id='product-options-wrapper']//select[contains(@class, 'product-custom-option admin__control-select')]"/> <element name="qtyInputWithProduct" type="input" selector="//tr//strong[contains(.,'{{productName}}')]/../../td[@class='col qty']//input" parameterized="true"/> <element name="customOptionRadio" type="input" selector="//span[contains(text(),'{{customOption}}')]/../../input" parameterized="true"/> + <element name="onlyProductsLeft" type="block" selector="//div[@class='product-info-price']//div[@class='product-info-stock-sku']//div[@class='availability only']"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml index a94610abf0918..6dcdde75bb2b7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml @@ -34,42 +34,46 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> <!-- Update product Advanced Inventory Setting --> - <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> - <waitForPageLoad stepKey="waitForProductToLoad"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> - <waitForPageLoad stepKey="waitForProductPageToSave"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> - <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetManageStockConfigActionGroup" stepKey="setManageStockConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminFillAdvancedInventoryQtyActionGroup" stepKey="fillProductQty"> + <argument name="qty" value="5"/> + </actionGroup> + <actionGroup ref="AdminSetMinAllowedQtyForProductActionGroup" stepKey="fillMiniAllowedQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetMaxAllowedQtyForProductActionGroup" stepKey="fillMaxAllowedQty"> + <argument name="qty" value="1000"/> + </actionGroup> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetNotifyBelowQtyValueActionGroup" stepKey="fillNotifyBelowQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetStockStatusConfigActionGroup" stepKey="selectOutOfStock"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Verify product is not visible in category store front page --> - <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> - <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="dontSeeProductInCategoryPage"/> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductInCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="{{SimpleProduct.name}}"/> + </actionGroup> <!--Verify Product In Store Front--> - <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToProductStorefrontPage"/> - <waitForPageLoad stepKey="waitForProductPageTobeLoaded"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="Out of stock" stepKey="seeProductStatusIsOutOfStock"/> + <actionGroup ref="StorefrontCheckProductStockStatus" stepKey="seeProductOnStorefront"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="stockStatus" value="Out of stock"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml index e64707a895fd4..cb6ae9244e958 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -37,41 +37,48 @@ <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0" /> </after> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> <!-- Update product Advanced Inventory Setting --> - <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> - <waitForPageLoad stepKey="waitForProductToLoad"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuantityUsesDecimal"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> - <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> - <waitForPageLoad stepKey="waitForProductPageToLoad"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> - <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> - + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetManageStockConfigActionGroup" stepKey="setManageStockConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminFillAdvancedInventoryQtyActionGroup" stepKey="fillProductQty"> + <argument name="qty" value="5"/> + </actionGroup> + <actionGroup ref="AdminSetMinAllowedQtyForProductActionGroup" stepKey="fillMiniAllowedQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetMaxAllowedQtyForProductActionGroup" stepKey="fillMaxAllowedQty"> + <argument name="qty" value="1000"/> + </actionGroup> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetNotifyBelowQtyValueActionGroup" stepKey="fillNotifyBelowQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetStockStatusConfigActionGroup" stepKey="selectOutOfStock"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Run re-index task --> <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <!--Verify product is visible in category front page --> - <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> - <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="selectCategory"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeProductName"> + <argument name="productName" value="{{SimpleProduct.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml index 192bab7c6d126..2cdec1405e9f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml @@ -31,21 +31,19 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedCategory"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> <!-- Verify Parent Category is visible in navigation menu and Sub category is not visible in navigation menu --> - <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnStoreNavigationBar"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCategoryOnStoreNavigationBar"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeSubCategoryOnStoreNavigation"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml index b8e58eae8a98a..83404391abca9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml @@ -23,16 +23,13 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="enterCategoryName"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="enterURLKey"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> - <!-- Literal URL below, need to refactor line + StorefrontCategoryPage when support for variable URL is implemented--> - <amOnPage url="/{{SimpleSubCategory.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> - <seeInTitle userInput="{{SimpleSubCategory.name}}" stepKey="assertTitle"/> - <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="assertInfo1"/> + <!--Go to storefront and verify created category on frontend--> + <actionGroup ref="CheckCategoryOnStorefrontActionGroup" stepKey="checkCreatedCategoryOnFrontend"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml index 4b0774d2307dd..e66984dda4427 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml @@ -21,10 +21,8 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> - <waitForPageLoad stepKey="waitStoreIndexPageLoad" /> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> - <argument name="storeGroupName" value="customStore.name"/> + <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCreatedNewRootCategory"> <argument name="categoryEntity" value="NewRootCategory"/> @@ -37,39 +35,32 @@ <argument name="categoryEntity" value="NewRootCategory"/> </actionGroup> <!--Create subcategory--> - <scrollToTopOfPage stepKey="scrollToTopOfPage"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name)}}" stepKey="clickOnCreatedNewRootCategory"/> - <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedCategory"> + <argument name="Category" value="NewRootCategory"/> + </actionGroup> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> <argument name="categoryEntity" value="SimpleSubCategory"/> </actionGroup> <!--Create a Store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Create a Store View--> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> - <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> - <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Go to store front page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front page--> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectCustomStore"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryInStoreFrontPage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="switchToCustomStore"> + <argument name="storeName" value="{{customStoreGroup.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml index 40bd3bdcfea20..4979b06a1051e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -27,16 +27,19 @@ <!--Verify Created root Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminCategoryBasicFieldSection.CategoryNameInput(NewRootCategory.name)}}" stepKey="seeRootCategory"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsListedInCategoriesTreeActionGroup" stepKey="seeRootCategory"> + <argument name="categoryName" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Delete Root Category--> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <!--Verify Root Category is not listed in backend--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories1"/> - <dontSee selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{NewRootCategory.name}}" stepKey="dontSeeRootCategory"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTheCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup" stepKey="doNotSeeRootCategory"> + <argument name="categoryName" value="{{NewRootCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml index fe07360d6b9ca..4310c6f06219a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml @@ -33,59 +33,48 @@ </after> <!--Create a Store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> - <see userInput="You saved the store." stepKey="seeSaveMessage"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Create a Store View--> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> - <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> - <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage1"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Go To store front page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front--> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectMainWebsite1"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeSubCategoryInStoreFront"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="selectCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Delete SubCategory--> <deleteData createDataKey="category" stepKey="deleteCategory"/> <!--Verify Sub Category is absent in backend --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories2"/> - <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="dontSeeCategoryInTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTheCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup" stepKey="doNotSeeRootCategory"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify Sub Category is not present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeOldCategoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> <!--Verify in Category is not in Url Rewrite grid--> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> - <waitForPageLoad stepKey="waitForUrlRewritePageTopLoad"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillRequestPath"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> + <actionGroup ref="AdminSearchDeletedUrlRewriteActionGroup" stepKey="searchingCategoryUrlRewrite"> + <argument name="requestPath" value="{{SimpleRootSubCategory.url_key}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml index 0ca8e74c4e59e..f4d464455491b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml @@ -32,16 +32,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!--Open store page --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <!--Create Custom Store --> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> <!--Create Store View--> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> @@ -50,32 +46,40 @@ </actionGroup> <!--Verify created SubCAtegory is present on Store Front --> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="seeCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToCategoryPage"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <!--Update Category--> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTreeUnderRoot(SimpleRootSubCategory.name)}}" stepKey="clickOnSubcategoryIsUndeRootCategory"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCateforyToSave"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandCategoryTree"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminChangeCategoryNameActionGroup" stepKey="updateCategoryName"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!--Verify the Category is not present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> - <waitForPageLoad stepKey="waitForPageToLoaded2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="dontSeeCatergoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeOldCategoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify the Updated Category is present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheUpdatedCategoryInStoreFront"/> - <waitForPageLoad stepKey="waitForPageToLoaded3"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeUpdatedCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml index 9b5fa25085e1a..48e6245b011ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml @@ -37,23 +37,26 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront2"/> - <waitForPageLoad stepKey="waitForCategoryStorefront"/> - <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeCreatedProduct"/> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="onCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$simpleSubCategory.name$$)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> - <waitForPageLoad stepKey="AdminCategoryEditPageLoad"/> - <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="EnableCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductOnCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedSubCategory"> + <argument name="Category" value="$$simpleSubCategory$$"/> + </actionGroup> + <actionGroup ref="AdminEnableCategoryActionGroup" stepKey="enableCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="seeSuccessMessage"/> <!--Run re-index task--> <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> - <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> - <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openEnabledCategory"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeCreatedProduct"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml new file mode 100644 index 0000000000000..507e4ae14e83c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckNoAppearDefaultOptionConfigurableProductTest"> + <annotations> + <stories value="Configurable Product"/> + <title value="Check for Configurable Product the default option doesn't appear."/> + <description value="Check for Configurable Product the default option doesn't appear on the list options product when an option use."/> + <testCaseId value="MC-35074"/> + <severity value="CRITICAL"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteAttribute"> + <argument name="productAttributeLabel" value="{{colorProductAttribute.default_label}}" /> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AdminFillBasicValueConfigurableProductActionGroup" stepKey="fillBasicValue"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup" stepKey="createOptions"/> + <actionGroup ref="AdminGotoSelectValueAttributePageActionGroup" stepKey="gotoSelectValuePage"> + <argument name="defaultLabelAttribute" value="{{colorProductAttribute.default_label}}"/> + </actionGroup> + <actionGroup ref="AdminSelectValueFromAttributeActionGroup" stepKey="selectColorProductAttribute2"> + <argument name="option" value="colorProductAttribute2"/> + </actionGroup> + <actionGroup ref="AdminSelectValueFromAttributeActionGroup" stepKey="selectColorProductAttribute3"> + <argument name="option" value="colorProductAttribute3"/> + </actionGroup> + <actionGroup ref="AdminSetQuantityToEachSkusConfigurableProductActionGroup" stepKey="saveConfigurable"/> + <grabValueFrom selector="{{NewProductPageSection.sku}}" stepKey="grabSkuProduct"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="expandOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="{{colorProductAttribute.default_label}}"/> + </actionGroup> + <dontSeeElement selector="{{LayeredNavigationSection.filterOptionContent(colorProductAttribute.default_label,colorProductAttribute1.name)}}" stepKey="dontSeeCaptchaField"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteConfigurableProduct"> + <argument name="sku" value="$grabSkuProduct"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml new file mode 100644 index 0000000000000..dc608a7f12dd3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.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="StorefrontOnlyXProductLeftForSimpleProductsTest"> + <annotations> + <features value="Catalog"/> + <title value="See Only * Left block"/> + <stories value="See Only * Left on product page if Only X left Threshold was set"/> + <description value="See Only * Left on product page if Only X left Threshold was set"/> + <testCaseId value="MC-35235"/> + <severity value="MINOR"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="config:set {{CatalogInventoryOptionsOnlyXleftThreshold.path}} 10000" stepKey="setStockThresholdQty"/> + <magentoCLI command="cache:flush config" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{CatalogInventoryOptionsOnlyXleftThreshold.path}} {{CatalogInventoryOptionsOnlyXleftThreshold.value}}" stepKey="removedStockThresholdQty"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <seeElement selector="{{StorefrontProductPageSection.onlyProductsLeft}}" stepKey="seeOnlyLeftBlock"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml new file mode 100644 index 0000000000000..914ac3444db22 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml @@ -0,0 +1,43 @@ +<?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="StorefrontRemoveProductFromCompareSidebarTest"> + <annotations> + <title value="Verify that the product isn't removed on clicking the product name"/> + <stories value="Verify that the product isn't removed on clicking the product name"/> + <description value="Verify that the product isn't removed on clicking the product name, but it's redirected to product page"/> + <features value="Catalog"/> + <severity value="MINOR"/> + <group value="Catalog"/> + <testCaseId value="MC-35068"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="SimpleProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + </after> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="category" value="$$defaultCategory$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addProductToCompareList"> + <argument name="productVar" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontClickOnProductFromSidebarCompareListActionGroup" stepKey="clickOnComparingProductLink"> + <argument name="product" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductPageUrl"> + <argument name="productUrl" value="$$simpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php index ca35d49113f41..681cef8489796 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php @@ -7,10 +7,12 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; +use Magento\Backend\Model\Session; use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation; use Magento\Catalog\Model\Product\AttributeSet\Build; use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; @@ -31,63 +33,64 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class SaveTest extends AttributeTest { /** * @var BuildFactory|MockObject */ - protected $buildFactoryMock; + private $buildFactoryMock; /** * @var FilterManager|MockObject */ - protected $filterManagerMock; + private $filterManagerMock; /** * @var ProductHelper|MockObject */ - protected $productHelperMock; + private $productHelperMock; /** * @var AttributeFactory|MockObject */ - protected $attributeFactoryMock; + private $attributeFactoryMock; /** * @var ValidatorFactory|MockObject */ - protected $validatorFactoryMock; + private $validatorFactoryMock; /** * @var CollectionFactory|MockObject */ - protected $groupCollectionFactoryMock; + private $groupCollectionFactoryMock; /** * @var LayoutFactory|MockObject */ - protected $layoutFactoryMock; + private $layoutFactoryMock; /** * @var ResultRedirect|MockObject */ - protected $redirectMock; + private $redirectMock; /** - * @var AttributeSet|MockObject + * @var AttributeSetInterface|MockObject */ - protected $attributeSetMock; + private $attributeSetMock; /** * @var Build|MockObject */ - protected $builderMock; + private $builderMock; /** * @var InputTypeValidator|MockObject */ - protected $inputTypeValidatorMock; + private $inputTypeValidatorMock; /** * @var FormData|MockObject @@ -104,19 +107,34 @@ class SaveTest extends AttributeTest */ private $attributeCodeValidatorMock; + /** + * @var Presentation|MockObject + */ + private $presentationMock; + + /** + * @var Session|MockObject + */ + + private $sessionMock; + protected function setUp(): void { parent::setUp(); + $this->filterManagerMock = $this->createMock(FilterManager::class); + $this->productHelperMock = $this->createMock(ProductHelper::class); + $this->attributeSetMock = $this->createMock(AttributeSetInterface::class); + $this->builderMock = $this->createMock(Build::class); + $this->inputTypeValidatorMock = $this->createMock(InputTypeValidator::class); + $this->formDataSerializerMock = $this->createMock(FormData::class); + $this->attributeCodeValidatorMock = $this->createMock(AttributeCodeValidator::class); + $this->presentationMock = $this->createMock(Presentation::class); + $this->sessionMock = $this->createMock(Session::class); + $this->layoutFactoryMock = $this->createMock(LayoutFactory::class); $this->buildFactoryMock = $this->getMockBuilder(BuildFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->filterManagerMock = $this->getMockBuilder(FilterManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productHelperMock = $this->getMockBuilder(ProductHelper::class) - ->disableOriginalConstructor() - ->getMock(); $this->attributeFactoryMock = $this->getMockBuilder(AttributeFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() @@ -129,32 +147,23 @@ protected function setUp(): void ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->layoutFactoryMock = $this->getMockBuilder(LayoutFactory::class) - ->disableOriginalConstructor() - ->getMock(); $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) ->setMethods(['setData', 'setPath']) ->disableOriginalConstructor() ->getMock(); - $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->builderMock = $this->getMockBuilder(Build::class) - ->disableOriginalConstructor() - ->getMock(); - $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) - ->disableOriginalConstructor() - ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) - ->setMethods(['getId', 'get']) - ->getMockForAbstractClass(); - + ->setMethods( + [ + 'getId', + 'get', + 'getBackendTypeByInput', + 'getDefaultValueByInput', + 'getBackendType', + 'getFrontendClass', + 'addData', + 'save' + ] + )->getMockForAbstractClass(); $this->buildFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->builderMock); @@ -167,7 +176,7 @@ protected function setUp(): void } /** - * {@inheritdoc} + * @inheritdoc */ protected function getModel() { @@ -184,7 +193,9 @@ protected function getModel() 'groupCollectionFactory' => $this->groupCollectionFactoryMock, 'layoutFactory' => $this->layoutFactoryMock, 'formDataSerializer' => $this->formDataSerializerMock, - 'attributeCodeValidator' => $this->attributeCodeValidatorMock + 'attributeCodeValidator' => $this->attributeCodeValidatorMock, + 'presentation' => $this->presentationMock, + '_session' => $this->sessionMock ]); } @@ -214,6 +225,67 @@ public function testExecuteWithEmptyData() $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); } + public function testExecuteSaveFrontendClass() + { + $data = [ + 'frontend_input' => 'test_frontend_input', + ]; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['isAjax', null, null], + ['serialized_options', '[]', ''], + ['set', null, 1], + ['attribute_code', null, 'test_attribute_code'], + ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with('') + ->willReturn([]); + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn($data); + $this->inputTypeValidatorMock->expects($this->any()) + ->method('isValid') + ->with($data['frontend_input']) + ->willReturn(true); + $this->presentationMock->expects($this->once()) + ->method('convertPresentationDataToInputType') + ->willReturn($data); + $this->productHelperMock->expects($this->once()) + ->method('getAttributeSourceModelByInputType') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productHelperMock->expects($this->once()) + ->method('getAttributeBackendModelByInputType') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productAttributeMock->expects($this->once()) + ->method('getBackendTypeByInput') + ->with($data['frontend_input']) + ->willReturnSelf('test_backend_type'); + $this->productAttributeMock->expects($this->once()) + ->method('getDefaultValueByInput') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productAttributeMock->expects($this->once()) + ->method('getBackendType') + ->willReturn('static'); + $this->productAttributeMock->expects($this->once()) + ->method('getFrontendClass') + ->willReturn('static'); + $this->resultFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->redirectMock); + $this->redirectMock->expects($this->any()) + ->method('setPath') + ->willReturnSelf(); + + $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); + } + public function testExecute() { $data = [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php new file mode 100644 index 0000000000000..c73e02fb7ecbf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Category\AggregateCount; +use Magento\Catalog\Model\ResourceModel\Category as ResourceCategory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Aggregate count model test + */ +class AggregateCountTest extends TestCase +{ + + /** + * @var AggregateCount + */ + protected $aggregateCount; + + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var Category|MockObject + */ + protected $categoryMock; + + /** + * @var ResourceCategory|MockObject + */ + protected $resourceCategoryMock; + + /** + * @var AdapterInterface|MockObject + */ + protected $connectionMock; + + /** + * {@inheritdoc} + */ + public function setUp(): void + { + $this->categoryMock = $this->createMock(Category::class); + $this->resourceCategoryMock = $this->createMock(ResourceCategory::class); + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->aggregateCount = $this->objectManagerHelper->getObject(AggregateCount::class); + } + + /** + * @return void + */ + public function testProcessDelete(): void + { + $parentIds = 3; + $table = 'catalog_category_entity'; + + $this->categoryMock->expects($this->once()) + ->method('getResource') + ->willReturn($this->resourceCategoryMock); + $this->categoryMock->expects($this->once()) + ->method('getParentIds') + ->willReturn($parentIds); + $this->resourceCategoryMock->expects($this->any()) + ->method('getEntityTable') + ->willReturn($table); + $this->resourceCategoryMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->connectionMock->expects($this->once()) + ->method('update') + ->with( + $table, + ['children_count' => new \Zend_Db_Expr('children_count - 1')], + ['entity_id IN(?)' => $parentIds] + ); + $this->aggregateCount->processDelete($this->categoryMock); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index 254d893d24584..17318d4207841 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -7,13 +7,16 @@ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; -use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Framework\AuthorizationInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; +use Magento\Backend\Model\Auth\Session; +use Magento\Authorization\Model\Role; +use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; /** @@ -51,6 +54,11 @@ class CategoriesTest extends AbstractModifierTest */ private $authorizationMock; + /** + * @var Session|MockObject + */ + private $sessionMock; + protected function setUp(): void { parent::setUp(); @@ -72,7 +80,10 @@ protected function setUp(): void $this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - + $this->sessionMock = $this->getMockBuilder(Session::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); $this->categoryCollectionFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->categoryCollectionMock); @@ -88,6 +99,26 @@ protected function setUp(): void $this->categoryCollectionMock->expects($this->any()) ->method('getIterator') ->willReturn(new \ArrayIterator([])); + + $roleAdmin = $this->getMockBuilder(Role::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $roleAdmin->expects($this->any()) + ->method('getId') + ->willReturn(0); + + $userAdmin = $this->getMockBuilder(User::class) + ->setMethods(['getRole']) + ->disableOriginalConstructor() + ->getMock(); + $userAdmin->expects($this->any()) + ->method('getRole') + ->willReturn($roleAdmin); + + $this->sessionMock->expects($this->any()) + ->method('getUser') + ->willReturn($userAdmin); } /** @@ -101,11 +132,28 @@ protected function createModel() 'locator' => $this->locatorMock, 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, 'arrayManager' => $this->arrayManagerMock, - 'authorization' => $this->authorizationMock + 'authorization' => $this->authorizationMock, + 'session' => $this->sessionMock ] ); } + /** + * @param object $object + * @param string $method + * @param array $args + * @return mixed + * @throws \ReflectionException + */ + private function invokeMethod($object, $method, $args = []) + { + $class = new \ReflectionClass(Categories::class); + $method = $class->getMethod($method); + $method->setAccessible(true); + + return $method->invokeArgs($object, $args); + } + public function testModifyData() { $this->assertSame([], $this->getModel()->modifyData([])); @@ -176,4 +224,44 @@ public function modifyMetaLockedDataProvider() { return [[true], [false]]; } + + /** + * Asserts that a user with an ACL role ID of 0 and a user with an ACL role ID of 1 do not have the same cache IDs + * Assumes a store ID of 0 + * + * @throws \ReflectionException + */ + public function testAclCacheIds() + { + $categoriesAdmin = $this->createModel(); + $cacheIdAdmin = $this->invokeMethod($categoriesAdmin, 'getCategoriesTreeCacheId', [0]); + + $roleAclUser = $this->getMockBuilder(Role::class) + ->disableOriginalConstructor() + ->getMock(); + $roleAclUser->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $userAclUser = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->getMock(); + $userAclUser->expects($this->any()) + ->method('getRole') + ->will($this->returnValue($roleAclUser)); + + $this->sessionMock = $this->getMockBuilder(Session::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); + + $this->sessionMock->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($userAclUser)); + + $categoriesAclUser = $this->createModel(); + $cacheIdAclUser = $this->invokeMethod($categoriesAclUser, 'getCategoriesTreeCacheId', [0]); + + $this->assertNotEquals($cacheIdAdmin, $cacheIdAclUser); + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 7608173c8edfc..c0d5f0a1af3b8 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -18,12 +18,14 @@ use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; use Magento\Framework\AuthorizationInterface; +use Magento\Backend\Model\Auth\Session; /** * Data provider for categories field of product page * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 101.0.0 */ class Categories extends AbstractModifier @@ -86,6 +88,11 @@ class Categories extends AbstractModifier */ private $authorization; + /** + * @var Session + */ + private $session; + /** * @param LocatorInterface $locator * @param CategoryCollectionFactory $categoryCollectionFactory @@ -94,6 +101,7 @@ class Categories extends AbstractModifier * @param ArrayManager $arrayManager * @param SerializerInterface $serializer * @param AuthorizationInterface $authorization + * @param Session $session */ public function __construct( LocatorInterface $locator, @@ -102,7 +110,8 @@ public function __construct( UrlInterface $urlBuilder, ArrayManager $arrayManager, SerializerInterface $serializer = null, - AuthorizationInterface $authorization = null + AuthorizationInterface $authorization = null, + Session $session = null ) { $this->locator = $locator; $this->categoryCollectionFactory = $categoryCollectionFactory; @@ -111,6 +120,7 @@ public function __construct( $this->arrayManager = $arrayManager; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class); + $this->session = $session ?: ObjectManager::getInstance()->get(Session::class); } /** @@ -370,10 +380,16 @@ protected function getCategoriesTree($filter = null) * @param string $filter * @return string */ - private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string + private function getCategoriesTreeCacheId(int $storeId, string $filter = ''): string { + if ($this->session->getUser() !== null) { + return self::CATEGORY_TREE_ID + . '_' . (string)$storeId + . '_' . $this->session->getUser()->getAclRole() + . '_' . $filter; + } return self::CATEGORY_TREE_ID - . '_' . (string) $storeId + . '_' . (string)$storeId . '_' . $filter; } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 0295e778f2b9b..dd757841410e2 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -40,7 +40,7 @@ use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory; /** - * Data provider for eav attributes on product page + * Class Eav data provider for product editing form * * @api * @@ -791,7 +791,9 @@ private function getAttributeDefaultValue(ProductAttributeInterface $attribute) \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $this->storeManager->getStore() ); - $attribute->setDefaultValue($defaultValue); + if ($defaultValue !== null) { + $attribute->setDefaultValue($defaultValue); + } } return $attribute->getDefaultValue(); } diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml index 1c97c920266df..a0aa48fb76b13 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -138,6 +138,11 @@ <index referenceId="CATALOG_PRODUCT_ENTITY_INT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> + <index referenceId="CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID_STORE_ID_VALUE" indexType="btree"> + <column name="attribute_id"/> + <column name="store_id"/> + <column name="value"/> + </index> </table> <table name="catalog_product_entity_text" resource="default" engine="innodb" comment="Catalog Product Text Attribute Backend Table"> diff --git a/app/code/Magento/Catalog/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json index d4bd6927d4345..f4cda73c371d0 100644 --- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json +++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json @@ -69,7 +69,8 @@ }, "index": { "CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID": true, - "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true + "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true, + "CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID_STORE_ID_VALUE": true }, "constraint": { "PRIMARY": true, diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php index 320e0adc29b9f..140659abfbfe6 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -8,6 +8,7 @@ namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation; use Magento\Framework\App\ResourceConnection; +use Magento\Store\Model\Store; /** * Fetch product attribute option data including attribute info @@ -41,16 +42,18 @@ public function __construct(ResourceConnection $resourceConnection) * Get option data. Return list of attributes with option data * * @param array $optionIds + * @param int|null $storeId * @param array $attributeCodes * @return array * @throws \Zend_Db_Statement_Exception */ - public function getOptions(array $optionIds, array $attributeCodes = []): array + public function getOptions(array $optionIds, ?int $storeId, array $attributeCodes = []): array { if (!$optionIds) { return []; } + $storeId = $storeId ?: Store::DEFAULT_STORE_ID; $connection = $this->resourceConnection->getConnection(); $select = $connection->select() ->from( @@ -70,9 +73,21 @@ public function getOptions(array $optionIds, array $attributeCodes = []): array ['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')], 'options.option_id = option_value.option_id', [ - 'option_label' => 'option_value.value', 'option_id' => 'option_value.option_id', ] + )->joinLeft( + ['option_value_store' => $this->resourceConnection->getTableName('eav_attribute_option_value')], + "options.option_id = option_value_store.option_id AND option_value_store.store_id = {$storeId}", + [ + 'option_label' => $connection->getCheckSql( + 'option_value_store.value_id > 0', + 'option_value_store.value', + 'option_value.value' + ) + ] + )->where( + 'a.attribute_id = options.attribute_id AND option_value.store_id = ?', + Store::DEFAULT_STORE_ID ); $select->where('option_value.option_id IN (?)', $optionIds); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index 0ec65c88024f2..105e91320de49 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -71,7 +71,7 @@ public function __construct( */ public function build(AggregationInterface $aggregation, ?int $storeId): array { - $attributeOptions = $this->getAttributeOptions($aggregation); + $attributeOptions = $this->getAttributeOptions($aggregation, $storeId); // build layer per attribute $result = []; @@ -133,10 +133,11 @@ private function isBucketEmpty(?BucketInterface $bucket): bool * Get list of attributes with options * * @param AggregationInterface $aggregation + * @param int|null $storeId * @return array * @throws \Zend_Db_Statement_Exception */ - private function getAttributeOptions(AggregationInterface $aggregation): array + private function getAttributeOptions(AggregationInterface $aggregation, ?int $storeId): array { $attributeOptionIds = []; $attributes = []; @@ -154,6 +155,6 @@ function (AggregationValueInterface $value) { return []; } - return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $attributes); + return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $storeId, $attributes); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 69592657241a0..0bfd9d58ec969 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -8,7 +8,10 @@ namespace Magento\CatalogGraphQl\Model; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; use Magento\Eav\Model\Entity\Collection\AbstractCollection; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Joins attributes for provided field node field names. @@ -43,11 +46,12 @@ public function __construct(array $fieldToAttributeMap = []) * * @param FieldNode $fieldNode * @param AbstractCollection $collection + * @param ResolveInfo $resolveInfo * @return void */ - public function join(FieldNode $fieldNode, AbstractCollection $collection): void + public function join(FieldNode $fieldNode, AbstractCollection $collection, ResolveInfo $resolveInfo): void { - foreach ($this->getQueryFields($fieldNode) as $field) { + foreach ($this->getQueryFields($fieldNode, $resolveInfo) as $field) { $this->addFieldToCollection($collection, $field); } } @@ -56,26 +60,70 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection): void * Get an array of queried fields. * * @param FieldNode $fieldNode + * @param ResolveInfo $resolveInfo * @return string[] */ - public function getQueryFields(FieldNode $fieldNode): array + public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array { if (null === $this->getFieldNodeSelections($fieldNode)) { $query = $fieldNode->selectionSet->selections; $selectedFields = []; + $fragmentFields = []; /** @var FieldNode $field */ foreach ($query as $field) { - if ($field->kind === 'InlineFragment') { - continue; + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field); + } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD && + ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) { + + foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) { + if (isset($spreadNode->selectionSet->selections)) { + $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo); + } else { + $selectedFields[] = $spreadNode->name->value; + } + } + } else { + $selectedFields[] = $field->name->value; } - $selectedFields[] = $field->name->value; } - $this->setSelectionsForFieldNode($fieldNode, $selectedFields); + if ($fragmentFields) { + $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields)); + } + $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } return $this->getFieldNodeSelections($fieldNode); } + /** + * Add fields from inline fragment nodes + * + * @param ResolveInfo $resolveInfo + * @param InlineFragmentNode $inlineFragmentField + * @param array $inlineFragmentFields + * @return string[] + */ + private function addInlineFragmentFields( + ResolveInfo $resolveInfo, + InlineFragmentNode $inlineFragmentField, + $inlineFragmentFields = [] + ): array { + $query = $inlineFragmentField->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($query as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields); + } elseif (isset($field->selectionSet->selections)) { + continue; + } else { + $inlineFragmentFields[] = $field->name->value; + } + } + + return array_unique($inlineFragmentFields); + } + /** * Add field to collection select * diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index b5d02511da4e7..ab100c7272ba0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -8,6 +8,9 @@ namespace Magento\CatalogGraphQl\Model\Category; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Used for determining the depth information for a requested category tree in a GraphQL request @@ -17,22 +20,57 @@ class DepthCalculator /** * Calculate the total depth of a category tree inside a GraphQL request * + * @param ResolveInfo $resolveInfo * @param FieldNode $fieldNode * @return int */ - public function calculate(FieldNode $fieldNode) : int + public function calculate(ResolveInfo $resolveInfo, FieldNode $fieldNode) : int { $selections = $fieldNode->selectionSet->selections ?? []; $depth = count($selections) ? 1 : 0; $childrenDepth = [0]; foreach ($selections as $node) { - if ($node->kind === 'InlineFragment' || null !== $node->alias) { + if (isset($node->alias) && null !== $node->alias) { continue; } - $childrenDepth[] = $this->calculate($node); + if ($node->kind === NodeKind::INLINE_FRAGMENT) { + $childrenDepth[] = $this->addInlineFragmentDepth($resolveInfo, $node); + } elseif ($node->kind === NodeKind::FRAGMENT_SPREAD && isset($resolveInfo->fragments[$node->name->value])) { + foreach ($resolveInfo->fragments[$node->name->value]->selectionSet->selections as $spreadNode) { + $childrenDepth[] = $this->calculate($resolveInfo, $spreadNode); + } + } else { + $childrenDepth[] = $this->calculate($resolveInfo, $node); + } } return $depth + max($childrenDepth); } + + /** + * Add inline fragment fields into calculating of category depth + * + * @param ResolveInfo $resolveInfo + * @param InlineFragmentNode $inlineFragmentField + * @param array $depth + * @return int + */ + private function addInlineFragmentDepth( + ResolveInfo $resolveInfo, + InlineFragmentNode $inlineFragmentField, + $depth = [] + ): int { + $selections = $inlineFragmentField->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($selections as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth); + } elseif ($field->selectionSet && $field->selectionSet->selections) { + $depth[] = $this->calculate($resolveInfo, $field); + } + } + + return $depth ? max($depth) : 0; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php index 5a230ceed0ca4..c6de07bdedd19 100644 --- a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php +++ b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class ProductLinksTypeResolver implements TypeResolverInterface { @@ -20,9 +20,9 @@ class ProductLinksTypeResolver implements TypeResolverInterface private $linkTypes = ['related', 'upsell', 'crosssell']; /** - * {@inheritdoc} + * @inheritdoc */ - public function resolveType(array $data) : string + public function resolveType(array $data): string { if (isset($data['link_type'])) { $linkType = $data['link_type']; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php index 535fe3a80cd25..d7118d71db89b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php @@ -7,18 +7,18 @@ namespace Magento\CatalogGraphQl\Model\Resolver; -use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; +use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; +use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; -use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Store\Model\StoreManagerInterface; /** @@ -121,7 +121,7 @@ function () use ($that, $categoryIds, $info) { } if (!$this->collection->isLoaded()) { - $that->attributesJoiner->join($info->fieldNodes[0], $this->collection); + $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info); $this->collection->addIdFilter($this->categoryIds); } /** @var CategoryInterface | \Magento\Catalog\Model\Category $item */ @@ -130,7 +130,7 @@ function () use ($that, $categoryIds, $info) { // Try to extract all requested fields from the loaded collection data $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true); $categories[$item->getId()]['model'] = $item; - $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0]); + $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info); $extractedFields = array_keys($categories[$item->getId()]); $foundFields = array_intersect($requestedFields, $extractedFields); if (count($requestedFields) === count($foundFields)) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php index 14732ecf37c63..187fd05c1001e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php @@ -22,7 +22,15 @@ class BatchProductLinks implements BatchServiceContractResolverInterface /** * @var string[] */ - private static $linkTypes = ['related', 'upsell', 'crosssell']; + private $linkTypes; + + /** + * @param array $linkTypes + */ + public function __construct(array $linkTypes) + { + $this->linkTypes = $linkTypes; + } /** * @inheritDoc @@ -44,7 +52,7 @@ public function convertToServiceArgument(ResolveRequestInterface $request) /** @var \Magento\Catalog\Model\Product $product */ $product = $value['model']; - return new ListCriteria((string)$product->getId(), self::$linkTypes, $product); + return new ListCriteria((string)$product->getId(), $this->linkTypes, $product); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php index 9ddad4e6451fa..3139c35774008 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use GraphQL\Language\AST\NodeKind; use Magento\Framework\GraphQl\Query\FieldTranslator; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -43,9 +44,9 @@ public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeN continue; } foreach ($node->selectionSet->selections as $selectionNode) { - if ($selectionNode->kind === 'InlineFragment') { + if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) { foreach ($selectionNode->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { + if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) { continue; } $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php index fc5a563c82b4e..c553d4486f9e9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php @@ -8,15 +8,16 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use GraphQL\Language\AST\FieldNode; -use Magento\CatalogGraphQl\Model\Category\DepthCalculator; -use Magento\CatalogGraphQl\Model\Category\LevelCalculator; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use GraphQL\Language\AST\NodeKind; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; -use Magento\Catalog\Model\Category; +use Magento\CatalogGraphQl\Model\Category\DepthCalculator; +use Magento\CatalogGraphQl\Model\Category\LevelCalculator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Category tree data provider @@ -85,8 +86,8 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato { $categoryQuery = $resolveInfo->fieldNodes[0]; $collection = $this->collectionFactory->create(); - $this->joinAttributesRecursively($collection, $categoryQuery); - $depth = $this->depthCalculator->calculate($categoryQuery); + $this->joinAttributesRecursively($collection, $categoryQuery, $resolveInfo); + $depth = $this->depthCalculator->calculate($resolveInfo, $categoryQuery); $level = $this->levelCalculator->calculate($rootCategoryId); // If root category is being filter, we've to remove first slash @@ -124,24 +125,27 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato * * @param Collection $collection * @param FieldNode $fieldNode + * @param ResolveInfo $resolveInfo * @return void */ - private function joinAttributesRecursively(Collection $collection, FieldNode $fieldNode) : void - { + private function joinAttributesRecursively( + Collection $collection, + FieldNode $fieldNode, + ResolveInfo $resolveInfo + ): void { if (!isset($fieldNode->selectionSet->selections)) { return; } $subSelection = $fieldNode->selectionSet->selections; - $this->attributesJoiner->join($fieldNode, $collection); + $this->attributesJoiner->join($fieldNode, $collection, $resolveInfo); /** @var FieldNode $node */ foreach ($subSelection as $node) { - if ($node->kind === 'InlineFragment') { + if ($node->kind === NodeKind::INLINE_FRAGMENT || $node->kind === NodeKind::FRAGMENT_SPREAD) { continue; } - - $this->joinAttributesRecursively($collection, $node); + $this->joinAttributesRecursively($collection, $node, $resolveInfo); } } } diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 5fec7bfd4fda7..03f9d7ad03f04 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -74,4 +74,14 @@ <preference type="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider" for="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface"/> <preference type="Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search" for="Magento\CatalogGraphQl\Model\Resolver\Products\Query\ProductQueryInterface"/> + + <type name="\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks"> + <arguments> + <argument name="linkTypes" xsi:type="array"> + <item name="related" xsi:type="string">related</item> + <item name="upsell" xsi:type="string">upsell</item> + <item name="crosssell" xsi:type="string">crosssell</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index c5fcac99767bd..189bfa61f2c42 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1595,6 +1595,7 @@ protected function _saveProducts() } $rowSku = $rowData[self::COL_SKU]; + $rowSkuNormalized = mb_strtolower($rowSku); if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); @@ -1604,9 +1605,9 @@ protected function _saveProducts() $storeId = !empty($rowData[self::COL_STORE]) ? $this->getStoreIdByCode($rowData[self::COL_STORE]) : Store::DEFAULT_STORE_ID; - $rowExistingImages = $existingImages[$storeId][$rowSku] ?? []; + $rowExistingImages = $existingImages[$storeId][$rowSkuNormalized] ?? []; $rowStoreMediaGalleryValues = $rowExistingImages; - $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSku] ?? []; + $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSkuNormalized] ?? []; if (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row @@ -1762,10 +1763,11 @@ protected function _saveProducts() continue; } - if (isset($rowExistingImages[$uploadedFile])) { - $currentFileData = $rowExistingImages[$uploadedFile]; + $uploadedFileNormalized = ltrim($uploadedFile, '/\\'); + if (isset($rowExistingImages[$uploadedFileNormalized])) { + $currentFileData = $rowExistingImages[$uploadedFileNormalized]; $currentFileData['store_id'] = $storeId; - $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFile]); + $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFileNormalized]); if (array_key_exists($uploadedFile, $imageHiddenStates) && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] ) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php index a45338c391a58..78ff26675930e 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php @@ -89,7 +89,6 @@ public function saveLinks( $resource = $this->linkFactory->create(); $mainTable = $resource->getMainTable(); $positionAttrId = []; - $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable); // pre-load 'position' attributes ID for each link type once foreach ($this->linkNameToId as $linkId) { @@ -103,6 +102,7 @@ public function saveLinks( $positionAttrId[$linkId] = $importEntity->getConnection()->fetchOne($select, $bind); } while ($bunch = $dataSourceModel->getNextBunch()) { + $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable); $this->processLinkBunches($importEntity, $linkField, $bunch, $resource, $nextLinkId, $positionAttrId); } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index a94a87a44b32a..d4694b72ba64f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -384,7 +384,9 @@ public function getExistingImages(array $bunch) foreach ($this->connection->fetchAll($select) as $image) { $storeId = $image['store_id']; unset($image['store_id']); - $result[$storeId][$image['sku']][$image['value']] = $image; + $sku = mb_strtolower($image['sku']); + $value = ltrim($image['value'], '/\\'); + $result[$storeId][$sku][$value] = $image; } return $result; diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index b050c6ae3b6ca..751fa465bdb17 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -37,7 +37,7 @@ </type> <type name="Magento\Catalog\Model\FilterProductCustomAttribute"> <arguments> - <argument name="blackList" xsi:type="array"> + <argument name="excludedList" xsi:type="array"> <item name="quantity_and_stock_status" xsi:type="string">quantity_and_stock_status</item> </argument> </arguments> diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php index 332bb991bf29f..b2aaa054ebc34 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php @@ -176,15 +176,16 @@ public function getCurrencyRate() * * @param float|string $fromPrice * @param float|string $toPrice + * @param boolean $isLast * @return float|\Magento\Framework\Phrase */ - protected function _renderRangeLabel($fromPrice, $toPrice) + protected function _renderRangeLabel($fromPrice, $toPrice, $isLast = false) { $fromPrice = empty($fromPrice) ? 0 : $fromPrice * $this->getCurrencyRate(); $toPrice = empty($toPrice) ? $toPrice : $toPrice * $this->getCurrencyRate(); $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === '') { + if ($isLast) { return __('%1 and above', $formattedFromPrice); } elseif ($fromPrice == $toPrice && $this->dataProvider->getOnePriceIntervalValue()) { return $formattedFromPrice; @@ -215,12 +216,15 @@ protected function _getItemsData() $data = []; if (count($facets) > 1) { // two range minimum + $lastFacet = array_key_last($facets); foreach ($facets as $key => $aggregation) { $count = $aggregation['count']; if (strpos($key, '_') === false) { continue; } - $data[] = $this->prepareData($key, $count, $data); + + $isLast = $lastFacet === $key; + $data[] = $this->prepareData($key, $count, $isLast); } } @@ -264,18 +268,13 @@ protected function getFrom($from) * * @param string $key * @param int $count + * @param boolean $isLast * @return array */ - private function prepareData($key, $count) + private function prepareData($key, $count, $isLast = false) { - list($from, $to) = explode('_', $key); - if ($from == '*') { - $from = $this->getFrom($to); - } - if ($to == '*') { - $to = $this->getTo($to); - } - $label = $this->_renderRangeLabel($from, $to); + [$from, $to] = explode('_', $key); + $label = $this->_renderRangeLabel($from, $to, $isLast); $value = $from . '-' . $to . $this->dataProvider->getAdditionalRequestData(); $data = [ diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml index c3f3865ef4549..c81540382c86f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml @@ -16,7 +16,8 @@ <argument name="selector" type="string"/> <argument name="userInput" type="string"/> </arguments> - + + <waitForElementVisible selector="{{selector}}" time="60" stepKey="waitForElementVisible"/> <see selector="{{selector}}" userInput="{{userInput}}" stepKey="assertElement"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml index e2d4fd2e89c2f..daa27b9918e47 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml @@ -19,6 +19,7 @@ <argument name="qty" type="string"/> </arguments> + <waitForElementVisible selector="{{CheckoutCartProductSection.productName}}" time="60" stepKey="waitForProductNameVisible"/> <see selector="{{CheckoutCartProductSection.productName}}" userInput="{{productName}}" stepKey="seeProductNameInCheckoutSummary"/> <see selector="{{CheckoutCartProductSection.ProductPriceByName(productName)}}" userInput="{{productPrice}}" stepKey="seeProductPriceInCart"/> <see selector="{{CheckoutCartProductSection.productSubtotalByName(productName)}}" userInput="{{subtotal}}" stepKey="seeSubtotalPrice"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml index e82f3c0588835..4185261993ffd 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontAddBundleDynamicProductToShoppingCartTest"> <annotations> + <features value="Checkout"/> <stories value="Shopping Cart"/> <title value="Add bundle dynamic product to the cart"/> <description value="Add bundle dynamic product to the cart"/> @@ -18,6 +19,7 @@ </annotations> <before> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> @@ -46,19 +48,23 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllRules"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="cataloginventory_stock"/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> </after> <!--Open Product page in StoreFront --> <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> - <argument name="product" value="$$createBundleProduct$$"/> + <argument name="product" value="$createBundleProduct$"/> </actionGroup> <!--Assert Product Price Range --> @@ -93,8 +99,8 @@ <!--Assert Product items in cart --> <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct1ItemsInCheckOutCart"> - <argument name="productName" value="$$createBundleProduct.name$$"/> - <argument name="productSku" value="$$createBundleProduct.sku$$"/> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="productSku" value="$createBundleProduct.sku$"/> <argument name="productPrice" value="$50.00"/> <argument name="subtotal" value="$100.00" /> <argument name="qty" value="2"/> @@ -107,13 +113,13 @@ </actionGroup> <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionInCart"> <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> - <argument name="userInput" value="1 x $$simpleProduct2.name$$ $50.00"/> + <argument name="userInput" value="1 x $simpleProduct2.name$ $50.00"/> </actionGroup> <!-- Assert Product in Mini Cart --> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> - <argument name="productName" value="$$createBundleProduct.name$$"/> + <argument name="productName" value="$createBundleProduct.name$"/> <argument name="productPrice" value="$50.00"/> <argument name="cartSubtotal" value="$100.00" /> <argument name="qty" value="2"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml index 6f16fa54a6ebf..ebf024490cce6 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml @@ -12,7 +12,7 @@ <element name="filterButton" type="input" selector="//button[text()='Filters']"/> <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']"/> - <element name="searchInput" type="input" selector="//*[@id='fulltext']"/> + <element name="searchInput" type="input" selector="#fulltext"/> <element name="searchButton" type="button" selector="//*[@id='fulltext']/parent::*/button"/> <element name="addNewPageButton" type="button" selector="#add" timeout="30"/> <element name="select" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//button[text()='Select']" parameterized="true"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml index 112335e726270..a6f4e7780d096 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="MediaGallerySection"> <element name="Browse" type="button" selector=".mce-i-browse"/> - <element name="browseForImage" type="button" selector="//*[@id='srcbrowser']"/> + <element name="browseForImage" type="button" selector="#srcbrowser"/> <element name="BrowseUploadImage" type="file" selector=".fileupload"/> <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> <element name="imageOrImageCopy" type="text" selector="//div[contains(@class,'media-gallery-modal')]//img[contains(@alt, '{{arg1}}.{{arg2}}')]|//img[contains(@alt,'{{arg1}}_') and contains(@alt,'.{{arg2}}')]" parameterized="true"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml index 1869a6544c3d3..5be91f61e1e1e 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml @@ -38,6 +38,8 @@ <element name="PageSize" type="input" selector="input[name='parameters[page_size]']"/> <element name="ProductAttribute" type="multiselect" selector="select[name='parameters[show_attributes][]']"/> <element name="ButtonToShow" type="multiselect" selector="select[name='parameters[show_buttons][]']"/> + <element name="InputAnchorCustomText" type="input" selector="input[name='parameters[anchor_text]']"/> + <element name="InputAnchorCustomTitle" type="input" selector="input[name='parameters[title]']"/> <!--Compare on Storefront--> <element name="ProductName" type="text" selector=".product.name.product-item-name"/> <element name="CompareBtn" type="button" selector=".action.tocompare"/> diff --git a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php index 257f8ba9bfe53..9e360481e8eb3 100644 --- a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php +++ b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php @@ -7,6 +7,7 @@ namespace Magento\CmsUrlRewrite\Plugin\Cms\Model\Store; +use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\PageRepositoryInterface; use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -21,6 +22,8 @@ */ class View { + private const ALL_STORE_VIEWS = '0'; + /** * @var UrlPersistInterface */ @@ -89,9 +92,8 @@ private function generateCmsPagesUrls(int $storeId): array { $rewrites = []; $urls = []; - $searchCriteria = $this->searchCriteriaBuilder->create(); - $cmsPagesCollection = $this->pageRepository->getList($searchCriteria)->getItems(); - foreach ($cmsPagesCollection as $page) { + + foreach ($this->getCmsPageItems() as $page) { $page->setStoreId($storeId); $rewrites[] = $this->cmsPageUrlRewriteGenerator->generate($page); } @@ -99,4 +101,18 @@ private function generateCmsPagesUrls(int $storeId): array return $urls; } + + /** + * Return cms page items for all store view + * + * @return PageInterface[] + */ + private function getCmsPageItems(): array + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter('store_id', self::ALL_STORE_VIEWS) + ->create(); + $list = $this->pageRepository->getList($searchCriteria); + + return $list->getItems(); + } } diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index 356c6ca17da18..f61e99529c3cc 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -208,6 +208,7 @@ public function save() ); $groupChangedPaths = $this->getChangedPaths($sectionId, $groupId, $groupData, $oldConfig, $extraOldGroups); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $changedPaths = \array_merge($changedPaths, $groupChangedPaths); } @@ -370,6 +371,7 @@ private function getChangedPaths( $oldConfig, $extraOldGroups ); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $changedPaths = \array_merge($changedPaths, $subGroupChangedPaths); } } @@ -435,11 +437,11 @@ protected function _processGroup( if (!isset($fieldData['value'])) { $fieldData['value'] = null; } - + if ($field->getType() == 'multiline' && is_array($fieldData['value'])) { $fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value'])); } - + $data = [ 'field' => $fieldId, 'groups' => $groups, @@ -453,7 +455,7 @@ protected function _processGroup( $backendModel->addData($data); $this->_checkSingleStoreMode($field, $backendModel); - $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig); + $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups); $backendModel->setPath($path)->setValue($fieldData['value']); $inherit = !empty($fieldData['inherit']); diff --git a/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php new file mode 100644 index 0000000000000..fbc45a9cfc791 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Block\DataProviders; + +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Provides permissions data into template. + */ +class PermissionsData implements ArgumentInterface +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor + * + * @param AuthorizationInterface $authorization + */ + public function __construct(AuthorizationInterface $authorization) + { + $this->authorization = $authorization; + } + + /** + * Check that user is allowed to manage attributes + * + * @return bool + */ + public function isAllowedToManageAttributes(): bool + { + return $this->authorization->isAllowed('Magento_Catalog::attributes_attributes'); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php index 1555e88700a45..2f333e7ca6f6e 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php @@ -4,11 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\ConfigurableProduct\Api\Data\OptionInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\ActionInterface; +/** + * Plugin product resource model + */ class Product { /** @@ -21,18 +31,45 @@ class Product */ private $productIndexer; + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + /** * Initialize Product dependencies. * * @param Configurable $configurable * @param ActionInterface $productIndexer + * @param ProductAttributeRepositoryInterface $productAttributeRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param FilterBuilder $filterBuilder */ public function __construct( Configurable $configurable, - ActionInterface $productIndexer + ActionInterface $productIndexer, + ProductAttributeRepositoryInterface $productAttributeRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null, + FilterBuilder $filterBuilder = null ) { $this->configurable = $configurable; $this->productIndexer = $productIndexer; + $this->productAttributeRepository = $productAttributeRepository ?: ObjectManager::getInstance() + ->get(ProductAttributeRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(SearchCriteriaBuilder::class); + $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance() + ->get(FilterBuilder::class); } /** @@ -41,6 +78,7 @@ public function __construct( * @param \Magento\Catalog\Model\ResourceModel\Product $subject * @param \Magento\Framework\DataObject $object * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -51,6 +89,39 @@ public function beforeSave( /** @var \Magento\Catalog\Model\Product $object */ if ($object->getTypeId() == Configurable::TYPE_CODE) { $object->getTypeInstance()->getSetAttributes($object); + $this->resetConfigurableOptionsData($object); + } + } + + /** + * Set null for configurable options attribute of configurable product + * + * @param \Magento\Catalog\Model\Product $object + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function resetConfigurableOptionsData($object) + { + $extensionAttribute = $object->getExtensionAttributes(); + if ($extensionAttribute && $extensionAttribute->getConfigurableProductOptions()) { + $attributeIds = []; + /** @var OptionInterface $option */ + foreach ($extensionAttribute->getConfigurableProductOptions() as $option) { + $attributeIds[] = $option->getAttributeId(); + } + + $filter = $this->filterBuilder + ->setField(ProductAttributeInterface::ATTRIBUTE_ID) + ->setConditionType('in') + ->setValue($attributeIds) + ->create(); + $this->searchCriteriaBuilder->addFilters([$filter]); + $searchCriteria = $this->searchCriteriaBuilder->create(); + $optionAttributes = $this->productAttributeRepository->getList($searchCriteria)->getItems(); + + foreach ($optionAttributes as $optionAttribute) { + $object->setData($optionAttribute->getAttributeCode(), null); + } } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml new file mode 100644 index 0000000000000..c48f22a3656d5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml @@ -0,0 +1,53 @@ +<?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="AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup"> + <annotations> + <description>Adds 3 provided Options to a new Attribute on the Configurable Product creation/edit page. Selected default first option. Set "Use in Layered Navigation" to "Yes".</description> + </annotations> + <arguments> + <argument name="label" defaultValue="colorProductAttribute" /> + <argument name="option1" defaultValue="colorProductAttribute1"/> + <argument name="option2" defaultValue="colorProductAttribute2"/> + <argument name="option3" defaultValue="colorProductAttribute3"/> + </arguments> + + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{label.default_label}}" stepKey="fillDefaultLabel"/> + + <!--Add option 1 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="{{option1.name}}" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> + <click selector="{{AdminNewAttributePanel.isDefault('1')}}" stepKey="selectDefault" after="fillAdminLabel1"/> + + <!--Add option 2 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption2" after="selectDefault"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('2')}}" time="30" stepKey="waitForOptionRow2" after="clickAddOption2"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('1')}}" userInput="{{option2.name}}" stepKey="fillAdminLabel2" after="waitForOptionRow2"/> + + <!--Add option 3 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption3" after="fillAdminLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('3')}}" time="30" stepKey="waitForOptionRow3" after="clickAddOption3"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('2')}}" userInput="{{option3.name}}" stepKey="fillAdminLabel3" after="waitForOptionRow3"/> + + <!-- Set Use In Layered Navigation --> + <click selector="{{AdminNewAttributePanel.storefrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab" after="fillAdminLabel3"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.storefrontPropertiesTitle}}" stepKey="waitTabLoad" after="goToStorefrontPropertiesTab"/> + <selectOption selector="{{AdminNewAttributePanel.useInLayeredNavigation}}" stepKey="selectUseInLayer" userInput="Filterable (with results)" after="waitTabLoad"/> + + <!--Save attribute--> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickSaveAttribute"/> + <waitForPageLoad stepKey="waitForSavingAttribute"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..cc709b80efebb --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml @@ -0,0 +1,32 @@ +<?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="AdminFillBasicValueConfigurableProductActionGroup"> + <annotations> + <description>Goes to the Admin Product grid page. Fill basic value for Configurable Product using the default Product Options.</description> + </annotations> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + <argument name="category" defaultValue="_defaultCategory"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="wait1"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="fillCategory"/> + <selectOption userInput="{{product.visibility}}" selector="{{AdminProductFormSection.visibility}}" stepKey="fillVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml new file mode 100644 index 0000000000000..969a41e27d459 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml @@ -0,0 +1,27 @@ +<?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="AdminGotoSelectValueAttributePageActionGroup"> + <annotations> + <description>Goes to the select values page from each attribute to include in the product.</description> + </annotations> + + <arguments> + <argument name="defaultLabelAttribute" type="string" defaultValue="{{colorProductAttribute.default_label}}"/> + </arguments> + + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{defaultLabelAttribute}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml new file mode 100644 index 0000000000000..cc2ff9a63ae40 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.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="AdminSelectValueFromAttributeActionGroup"> + <annotations> + <description>Click to check option.</description> + </annotations> + + <arguments> + <argument name="option" defaultValue="colorProductAttribute1"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeOption(option.name)}}" stepKey="clickOnCreateNewValue2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..3cca319d9569c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.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="AdminSetQuantityToEachSkusConfigurableProductActionGroup"> + <annotations> + <description>Set quantity 1 to all child skus for configurable product. Save a configurable product and confirm.</description> + </annotations> + + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="1" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php index abab103fa6d37..3d5a0d1cc6a3f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php @@ -7,20 +7,38 @@ namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Model\ResourceModel; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product as ModelProduct; use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductAttributeSearchResults; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; +use Magento\Catalog\Model\ResourceModel\Product as ResourceModelProduct; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute; +use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product as PluginResourceModelProduct; +use Magento\Framework\Api\ExtensionAttributesInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Indexer\ActionInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ProductTest extends TestCase { /** - * @var ObjectManager + * @var PluginResourceModelProduct */ - private $objectManager; + private $model; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; /** * @var Configurable|MockObject @@ -33,39 +51,128 @@ class ProductTest extends TestCase private $actionMock; /** - * @var Product + * @var ProductAttributeRepositoryInterface|MockObject */ - private $model; + private $productAttributeRepositoryMock; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var FilterBuilder|MockObject + */ + private $filterBuilderMock; protected function setUp(): void { - $this->objectManager = new ObjectManager($this); $this->configurableMock = $this->createMock(Configurable::class); $this->actionMock = $this->getMockForAbstractClass(ActionInterface::class); - - $this->model = $this->objectManager->getObject( - Product::class, + $this->productAttributeRepositoryMock = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getList']) + ->getMockForAbstractClass(); + $this->searchCriteriaBuilderMock = $this->createPartialMock( + SearchCriteriaBuilder::class, + ['addFilters', 'create'] + ); + $this->filterBuilderMock = $this->createPartialMock( + FilterBuilder::class, + ['setField', 'setConditionType', 'setValue', 'create'] + ); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + PluginResourceModelProduct::class, [ 'configurable' => $this->configurableMock, 'productIndexer' => $this->actionMock, + 'productAttributeRepository' => $this->productAttributeRepositoryMock, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'filterBuilder' => $this->filterBuilderMock ] ); } - public function testBeforeSaveConfigurable() + public function testBeforeSaveConfigurable(): void { - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $object */ - $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']); + /** @var ResourceModelProduct|MockObject $subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $object */ + $object = $this->createPartialMock( + ModelProduct::class, + [ + 'getTypeId', + 'getTypeInstance', + 'getExtensionAttributes', + 'setData' + ] + ); $type = $this->createPartialMock( Configurable::class, ['getSetAttributes'] ); - $type->expects($this->once())->method('getSetAttributes')->with($object); - - $object->expects($this->once())->method('getTypeId')->willReturn(Configurable::TYPE_CODE); - $object->expects($this->once())->method('getTypeInstance')->willReturn($type); + $extensionAttributes = $this->getMockBuilder(ExtensionAttributesInterface::class) + ->disableOriginalConstructor() + ->addMethods(['getConfigurableProductOptions']) + ->getMock(); + $option = $this->createPartialMock( + ConfigurableAttribute::class, + ['getAttributeId'] + ); + $extensionAttributes->expects($this->exactly(2)) + ->method('getConfigurableProductOptions') + ->willReturn([$option]); + $object->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($extensionAttributes); + + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setField') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setValue') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setConditionType') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturnSelf(); + $searchCriteria = $this->createMock(SearchCriteria::class); + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($searchCriteria); + $searchResultMockClass = $this->createPartialMock( + ProductAttributeSearchResults::class, + ['getItems'] + ); + $this->productAttributeRepositoryMock->expects($this->once()) + ->method('getList') + ->with($searchCriteria) + ->willReturn($searchResultMockClass); + $optionAttribute = $this->createPartialMock( + EavAttribute::class, + ['getAttributeCode'] + ); + $searchResultMockClass->expects($this->once()) + ->method('getItems') + ->willReturn([$optionAttribute]); + $type->expects($this->once()) + ->method('getSetAttributes') + ->with($object); + $object->expects($this->once()) + ->method('getTypeId') + ->will($this->returnValue(Configurable::TYPE_CODE)); + $object->expects($this->once()) + ->method('getTypeInstance') + ->will($this->returnValue($type)); + $object->expects($this->once()) + ->method('setData'); + $option->expects($this->once()) + ->method('getAttributeId'); + $optionAttribute->expects($this->once()) + ->method('getAttributeCode'); $this->model->beforeSave( $subject, @@ -73,14 +180,23 @@ public function testBeforeSaveConfigurable() ); } - public function testBeforeSaveSimple() + public function testBeforeSaveSimple(): void { - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $object */ - $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']); - $object->expects($this->once())->method('getTypeId')->willReturn(Type::TYPE_SIMPLE); - $object->expects($this->never())->method('getTypeInstance'); + /** @var ResourceModelProduct|MockObject$subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $object */ + $object = $this->createPartialMock( + ModelProduct::class, + [ + 'getTypeId', + 'getTypeInstance' + ] + ); + $object->expects($this->once()) + ->method('getTypeId') + ->will($this->returnValue(Type::TYPE_SIMPLE)); + $object->expects($this->never()) + ->method('getTypeInstance'); $this->model->beforeSave( $subject, @@ -88,29 +204,35 @@ public function testBeforeSaveSimple() ); } - public function testAroundDelete() + public function testAroundDelete(): void { $productId = '1'; $parentConfigId = ['2']; - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $product */ + /** @var ResourceModelProduct|MockObject $subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $product */ $product = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, + ModelProduct::class, ['getId', 'delete'] ); - $product->expects($this->once())->method('getId')->willReturn($productId); - $product->expects($this->once())->method('delete')->willReturn(true); + $product->expects($this->once()) + ->method('getId') + ->willReturn($productId); + $product->expects($this->once()) + ->method('delete') + ->willReturn(true); $this->configurableMock->expects($this->once()) ->method('getParentIdsByChild') ->with($productId) ->willReturn($parentConfigId); - $this->actionMock->expects($this->once())->method('executeList')->with($parentConfigId); + $this->actionMock->expects($this->once()) + ->method('executeList') + ->with($parentConfigId); $return = $this->model->aroundDelete( $subject, - /** @var \Magento\Catalog\Model\Product|MockObject $prod */ - function (\Magento\Catalog\Model\Product $prod) use ($subject) { + /** @var ModelProduct|MockObject $prod */ + function (ModelProduct $prod) use ($subject) { $prod->delete(); return $subject; }, diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml index a084abfc31eaa..ffd17a8bf4734 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml @@ -48,6 +48,7 @@ <item name="modal" xsi:type="string">configurableModal</item> <item name="dataScope" xsi:type="string">productFormConfigurable</item> </argument> + <argument name="permissions" xsi:type="object">Magento\ConfigurableProduct\Block\DataProviders\PermissionsData</argument> </arguments> </block> <block class="Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Bulk" name="step3" template="Magento_ConfigurableProduct::catalog/product/edit/attribute/steps/bulk.phtml"> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml index e996df8260719..e94d94e0ded55 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml @@ -5,6 +5,9 @@ */ /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */ +$isAllowedToManageAttributes = $block->getPermissions()->isAllowedToManageAttributes(); +$attributesUrl = $block->getUrl('catalog/product_attribute/getAttributes'); +$optionsUrl = $block->getUrl('catalog/product_attribute/createOptions'); ?> <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> <h2 class="steps-wizard-title"><?= $block->escapeHtml( @@ -12,7 +15,8 @@ ); ?></h2> <div class="steps-wizard-info"> <span><?= $block->escapeHtml( - __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.') + __('Select values from each attribute to include in this product. ' . + 'Each unique combination of values creates a unique product SKU.') );?></span> </div> <div data-bind="foreach: attributes, sortableList: attributes"> @@ -72,7 +76,8 @@ <label data-bind="text: label, visible: label, attr:{for:id}" class="admin__field-label"></label> </div> - <div class="admin__field admin__field-create-new" data-bind="attr:{'data-role':id}, visible: !label"> + <div class="admin__field admin__field-create-new" + data-bind="attr:{'data-role':id}, visible: !label"> <div class="admin__field-control"> <input class="admin__control-text" name="label" @@ -101,14 +106,14 @@ </li> </ul> </fieldset> - <button class="action-create-new action-tertiary" - type="button" - data-action="addOption" - data-bind="click: $parent.createOption, visible: canCreateOption"> - <span><?= $block->escapeHtml( - __('Create New Value') - ); ?></span> - </button> + <?php if ($isAllowedToManageAttributes): ?> + <button class="action-create-new action-tertiary" + type="button" + data-action="addOption" + data-bind="click: $parent.createOption, visible: canCreateOption"> + <span><?= $block->escapeHtml(__('Create New Value')); ?></span> + </button> + <?php endif; ?> </div> </div> </div> @@ -120,8 +125,8 @@ "<?= /* @noEscape */ $block->getComponentName() ?>": { "component": "Magento_ConfigurableProduct/js/variations/steps/attributes_values", "appendTo": "<?= /* @noEscape */ $block->getParentComponentName() ?>", - "optionsUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product_attribute/getAttributes') ?>", - "createOptionsUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product_attribute/createOptions') ?>" + "optionsUrl": "<?= /* @noEscape */ $attributesUrl ?>", + "createOptionsUrl": "<?= /* @noEscape */ $optionsUrl ?>" } } } diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml index d218e650657ac..eee9f742a59a4 100644 --- a/app/code/Magento/Contact/view/frontend/templates/form.phtml +++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml @@ -4,6 +4,9 @@ * See COPYING.txt for license details. */ +// phpcs:disable Magento2.Templates.ThisInTemplate +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Contact\Block\ContactForm $block */ /** @var \Magento\Contact\ViewModel\UserDataProvider $viewModel */ @@ -23,35 +26,35 @@ $viewModel = $block->getViewModel(); <div class="field name required"> <label class="label" for="name"><span><?= $block->escapeHtml(__('Name')) ?></span></label> <div class="control"> - <input name="name" - id="name" - title="<?= $block->escapeHtmlAttr(__('Name')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserName()) ?>" - class="input-text" - type="text" + <input name="name" + id="name" + title="<?= $block->escapeHtmlAttr(__('Name')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserName()) ?>" + class="input-text" + type="text" data-validate="{required:true}"/> </div> </div> <div class="field email required"> <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> - <input name="email" - id="email" - title="<?= $block->escapeHtmlAttr(__('Email')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserEmail()) ?>" - class="input-text" - type="email" + <input name="email" + id="email" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserEmail()) ?>" + class="input-text" + type="email" data-validate="{required:true, 'validate-email':true}"/> </div> </div> <div class="field telephone"> <label class="label" for="telephone"><span><?= $block->escapeHtml(__('Phone Number')) ?></span></label> <div class="control"> - <input name="telephone" - id="telephone" - title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserTelephone()) ?>" - class="input-text" + <input name="telephone" + id="telephone" + title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserTelephone()) ?>" + class="input-text" type="tel" /> </div> </div> @@ -60,12 +63,12 @@ $viewModel = $block->getViewModel(); <span><?= $block->escapeHtml(__('What’s on your mind?')) ?></span> </label> <div class="control"> - <textarea name="comment" - id="comment" - title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" - class="input-text" - cols="5" - rows="3" + <textarea name="comment" + id="comment" + title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" + class="input-text" + cols="5" + rows="3" data-validate="{required:true}"><?= $block->escapeHtml($viewModel->getUserComment()) ?> </textarea> </div> @@ -81,3 +84,12 @@ $viewModel = $block->getViewModel(); </div> </div> </form> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "contact-form" + } + } + } +</script> diff --git a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php index 2be340c8ccca4..b4c737f6600bf 100644 --- a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php +++ b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php @@ -52,7 +52,7 @@ public function getFrontendLabel(string $attributeCode): string { try { $attribute = $this->addressMetadata->getAttributeMetadata($attributeCode); - $frontendLabel = $attribute->getFrontendLabel(); + $frontendLabel = $attribute->getStoreLabel() ?: $attribute->getFrontendLabel(); } catch (NoSuchEntityException $e) { $frontendLabel = ''; } diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php index d68904f6d1645..f2868132790cf 100644 --- a/app/code/Magento/Customer/Model/CustomerRegistry.php +++ b/app/code/Magento/Customer/Model/CustomerRegistry.php @@ -101,8 +101,10 @@ public function retrieve($customerId) public function retrieveByEmail($customerEmail, $websiteId = null) { if ($websiteId === null) { - $websiteId = $this->storeManager->getStore()->getWebsiteId(); + $websiteId = $this->storeManager->getStore()->getWebsiteId() + ?: $this->storeManager->getDefaultStoreView()->getWebsiteId(); } + $emailKey = $this->getEmailKey($customerEmail, $websiteId); if (isset($this->customerRegistryByEmail[$emailKey])) { return $this->customerRegistryByEmail[$emailKey]; diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php index 9eb9ffb806c9f..b877b2cca67a5 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php @@ -7,7 +7,10 @@ namespace Magento\Customer\Model\Plugin; use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; use Magento\Integration\Api\AuthorizationServiceInterface as AuthorizationService; +use Magento\Store\Model\StoreManagerInterface; /** * Plugin around \Magento\Framework\Authorization::isAllowed @@ -19,16 +22,41 @@ class CustomerAuthorization /** * @var UserContextInterface */ - protected $userContext; + private $userContext; + + /** + * @var CustomerFactory + */ + private $customerFactory; + + /** + * @var CustomerResource + */ + private $customerResource; + + /** + * @var StoreManagerInterface + */ + private $storeManager; /** * Inject dependencies. * * @param UserContextInterface $userContext + * @param CustomerFactory $customerFactory + * @param CustomerResource $customerResource + * @param StoreManagerInterface $storeManager */ - public function __construct(UserContextInterface $userContext) - { + public function __construct( + UserContextInterface $userContext, + CustomerFactory $customerFactory, + CustomerResource $customerResource, + StoreManagerInterface $storeManager + ) { $this->userContext = $userContext; + $this->customerFactory = $customerFactory; + $this->customerResource = $customerResource; + $this->storeManager = $storeManager; } /** @@ -53,9 +81,15 @@ public function aroundIsAllowed( && $this->userContext->getUserId() && $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER ) { - return true; - } else { - return $proceed($resource, $privilege); + $customer = $this->customerFactory->create(); + $this->customerResource->load($customer, $this->userContext->getUserId()); + $currentStoreId = $this->storeManager->getStore()->getId(); + $sharedStoreIds = $customer->getSharedStoreIds(); + if (in_array($currentStoreId, $sharedStoreIds)) { + return true; + } } + + return $proceed($resource, $privilege); } } diff --git a/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php new file mode 100644 index 0000000000000..fdde31e05fb2e --- /dev/null +++ b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Model\Plugin; + +use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; + +/** + * Update customer by id from request param + */ +class UpdateCustomer +{ + /** + * @var RestRequest + */ + private $request; + + /** + * @param RestRequest $request + */ + public function __construct(RestRequest $request) + { + $this->request = $request; + } + + /** + * Update customer by id from request if exist + * + * @param CustomerRepositoryInterface $customerRepository + * @param CustomerInterface $customer + * @param string|null $passwordHash + * @return array + */ + public function beforeSave( + CustomerRepositoryInterface $customerRepository, + CustomerInterface $customer, + ?string $passwordHash = null + ): array { + $customerId = $this->request->getParam('customerId'); + + if ($customerId) { + $customer = $this->getUpdatedCustomer($customerRepository->getById($customerId), $customer); + } + + return [$customer, $passwordHash]; + } + + /** + * Return updated customer + * + * @param CustomerInterface $originCustomer + * @param CustomerInterface $customer + * @return CustomerInterface + */ + private function getUpdatedCustomer( + CustomerInterface $originCustomer, + CustomerInterface $customer + ): CustomerInterface { + $newCustomer = clone $originCustomer; + foreach ($customer->__toArray() as $name => $value) { + if ($name === CustomerInterface::CUSTOM_ATTRIBUTES) { + $value = $customer->getCustomAttributes(); + } elseif ($name === CustomerInterface::EXTENSION_ATTRIBUTES_KEY) { + $value = $customer->getExtensionAttributes(); + } elseif ($name === CustomerInterface::KEY_ADDRESSES) { + $value = $customer->getAddresses(); + } + + $newCustomer->setData($name, $value); + } + + return $newCustomer; + } +} diff --git a/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php new file mode 100644 index 0000000000000..c2b7189b808a3 --- /dev/null +++ b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Observer; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\Customer\Model\Data\Customer; + +/** + * Class observer UpgradeOrderCustomerEmailObserver + * Update orders customer email after corresponding customer email changed + */ +class UpgradeOrderCustomerEmailObserver implements ObserverInterface +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param OrderRepositoryInterface $orderRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + OrderRepositoryInterface $orderRepository, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->orderRepository = $orderRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Upgrade order customer email when customer has changed email + * + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer): void + { + /** @var Customer $originalCustomer */ + $originalCustomer = $observer->getEvent()->getOrigCustomerDataObject(); + if (!$originalCustomer) { + return; + } + + /** @var Customer $customer */ + $customer = $observer->getEvent()->getCustomerDataObject(); + $customerEmail = $customer->getEmail(); + + if ($customerEmail === $originalCustomer->getEmail()) { + return; + } + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + /** + * @var Collection $orders + */ + $orders = $this->orderRepository->getList($searchCriteria); + $orders->setDataToAll(OrderInterface::CUSTOMER_EMAIL, $customerEmail); + $orders->save(); + } +} diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml new file mode 100644 index 0000000000000..b827cba8490b8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.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="AdminCustomerDeleteWishlistItemActionGroup"> + <click selector="{{AdminCustomerWishlistSection.deleteButton}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForResultsLoading"/> + <click selector="{{AdminCustomerWishlistSection.deleteConfirm}}" stepKey="confirmDeleting"/> + <waitForPageLoad stepKey="waitForPageLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml new file mode 100644 index 0000000000000..bbdc4de330840 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCustomerFindWishlistItemActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <fillField userInput="{{productName}}" selector="{{AdminCustomerWishlistSection.productName}}" stepKey="fillProductNameField"/> + <click selector="{{AdminCustomerWishlistSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForGridLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml new file mode 100644 index 0000000000000..66b464006aa0f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.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="AdminNavigateCustomerWishlistTabActionGroup"> + <click selector="{{AdminCustomerInformationSection.wishList}}" stepKey="clickWishlistButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml new file mode 100644 index 0000000000000..16688be61171e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AssertAdminCustomerNoItemsInWishlistActionGroup"> + <see userInput="No Items Found" selector="{{AdminCustomerWishlistSection.gridTable}}" stepKey="assertNoItems"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml new file mode 100644 index 0000000000000..f287c728bb28d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml @@ -0,0 +1,34 @@ +<?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="CreateCustomerOrderActionGroup"> + <annotations> + <description>Create Order via API assigned to Customer.</description> + </annotations> + + <createData entity="CustomerCart" stepKey="CustomerCart"> + <requiredEntity createDataKey="Customer"/> + </createData> + + <createData entity="CustomerCartItem" stepKey="addCartItem"> + <requiredEntity createDataKey="CustomerCart"/> + <requiredEntity createDataKey="Product"/> + </createData> + + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="CustomerCart"/> + </createData> + + <updateData createDataKey="CustomerCart" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformation"> + <requiredEntity createDataKey="CustomerCart"/> + </updateData> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml new file mode 100644 index 0000000000000..5dafe59bf3c48 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.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="StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup"> + <arguments> + <argument name="itemName" type="string"/> + </arguments> + <dontSee userInput="{{itemName}}" selector="{{StorefrontCustomerSidebarSection.sidebarTab(itemName)}}" stepKey="dontSeeElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml new file mode 100644 index 0000000000000..39a67968c66e4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.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="AdminCustomerWishlistSection"> + <element name="productName" type="input" selector="#wishlistGrid_filter_product_name"/> + <element name="searchButton" type="button" selector="#wishlistGrid button[data-action='grid-filter-apply']"/> + <element name="deleteButton" type="text" selector="//*[@id='wishlistGrid_table']//*[@data-column='action']//*[text()='Delete']"/> + <element name="deleteConfirm" type="button" selector=".modal-popup.confirm .action-primary.action-accept"/> + <element name="gridTable" type="text" selector="#wishlistGrid_table"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml index ec5141d84b1bd..61ce050aa3ef2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml @@ -17,5 +17,9 @@ <element name="viewOrder" type="button" selector="//td[contains(concat(' ',normalize-space(@class),' '),' col actions ')]/a[contains(concat(' ',normalize-space(@class),' '),' action view ')]"/> <element name="tabRefund" type="button" selector="//a[text()='Refunds']"/> <element name="grandTotalRefund" type="text" selector="td[data-th='Grand Total'] > strong > span.price"/> + <element name="currentPage" type="text" selector=".order-products-toolbar .pages .current span:nth-of-type(2)"/> + <element name="pageNumber" type="text" selector="//*[@class='order-products-toolbar toolbar bottom']//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> + <element name="perPage" type="select" selector="//*[@class='order-products-toolbar toolbar bottom']//select[@id='limiter']"/> + <element name="rowsInColumn" type="text" selector="//tbody/tr/td[contains(@class, '{{column}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml new file mode 100644 index 0000000000000..ba113c739d706 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml @@ -0,0 +1,144 @@ +<?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="StorefrontCustomerAccountOrderListTest"> + <annotations> + <stories value="Frontend Customer Account Orders list"/> + <title value="Verify that the list of Orders is displayed in the grid after changing the number of items on the page"/> + <description value="Verify that the list of Orders is displayed in the grid after changing the number of items on the page."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-34953"/> + <group value="customer"/> + </annotations> + + <before> + + <!--Create Product via API--> + <createData entity="SimpleProduct2" stepKey="Product"/> + + <!--Create Customer via API--> + <createData entity="Simple_US_Customer" stepKey="Customer"/> + + <!--Create Orders via API--> + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder1"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder2"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder3"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder4"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder5"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder6"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder7"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder8"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder9"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder10"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder11"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder12"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder13"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder14"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder15"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + <!--Create Orders via API--> + + </before> + + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <deleteData createDataKey="Product" stepKey="deleteProduct"/> + <deleteData createDataKey="Customer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$Customer$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu"> + <argument name="menu" value="My Orders"/> + </actionGroup> + + <seeElement selector="{{StorefrontCustomerOrderSection.isMyOrdersSection}}" stepKey="waitOrderHistoryPage"/> + + <scrollTo selector="{{StorefrontCustomerOrderSection.currentPage}}" stepKey="scrollToBottomToolbarSection"/> + + <click selector="{{StorefrontCustomerOrderSection.pageNumber('2')}}" stepKey="clickOnPage2"/> + + <scrollTo selector="{{StorefrontCustomerOrderSection.perPage}}" stepKey="scrollToLimiter"/> + + <selectOption userInput="20" selector="{{StorefrontCustomerOrderSection.perPage}}" stepKey="selectLimitOnPage"/> + + <waitForPageLoad stepKey="waitForLoadPage"/> + + <seeElement selector="{{StorefrontCustomerOrderSection.isMyOrdersSection}}" + stepKey="seeElementOrderHistoryPage"/> + + <dontSee selector="{{StorefrontOrderInformationMainSection.emptyMessage}}" + userInput="You have placed no orders." stepKey="dontSeeEmptyMessage"/> + + <seeNumberOfElements selector="{{StorefrontCustomerOrderSection.rowsInColumn('id')}}" userInput="15" + stepKey="seeRowsCount"/> + + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php new file mode 100644 index 0000000000000..d05c10c00e6c3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Observer; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Observer\UpgradeOrderCustomerEmailObserver; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * For testing upgrade order customer email + */ +class UpgradeOrderCustomerEmailObserverTest extends TestCase +{ + private const NEW_CUSTOMER_EMAIL = "test@test.com"; + private const ORIGINAL_CUSTOMER_EMAIL = "origtest@test.com"; + + /** + * @var UpgradeOrderCustomerEmailObserver + */ + private $orderCustomerEmailObserver; + + /** + * @var Observer|MockObject + */ + private $observerMock; + + /** + * @var OrderRepositoryInterface|MockObject + */ + private $orderRepositoryMock; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var Event|MockObject + */ + private $eventMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->getMock(); + + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerDataObject', 'getOrigCustomerDataObject']) + ->getMock(); + + $this->observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->observerMock->expects($this->any())->method('getEvent')->willReturn($this->eventMock); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->orderCustomerEmailObserver = $this->objectManagerHelper->getObject( + UpgradeOrderCustomerEmailObserver::class, + [ + 'orderRepository' => $this->orderRepositoryMock, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + ] + ); + } + + /** + * Verifying that the order email is not updated when the customer email is not updated + * + */ + public function testUpgradeOrderCustomerEmailWhenMailIsNotChanged(): void + { + $customer = $this->createCustomerMock(); + $originalCustomer = $this->createCustomerMock(); + + $this->setCustomerToEventMock($customer); + $this->setOriginalCustomerToEventMock($originalCustomer); + + $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL); + $this->setCustomerEmail($customer, self::ORIGINAL_CUSTOMER_EMAIL); + + $this->whenOrderRepositoryGetListIsNotCalled(); + + $this->orderCustomerEmailObserver->execute($this->observerMock); + } + + /** + * Verifying that the order email is updated after the customer updates their email + * + */ + public function testUpgradeOrderCustomerEmail(): void + { + $customer = $this->createCustomerMock(); + $originalCustomer = $this->createCustomerMock(); + $orderCollectionMock = $this->createOrderMock(); + + $this->setCustomerToEventMock($customer); + $this->setOriginalCustomerToEventMock($originalCustomer); + + $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL); + $this->setCustomerEmail($customer, self::NEW_CUSTOMER_EMAIL); + + $this->whenOrderRepositoryGetListIsCalled($orderCollectionMock); + + $this->whenOrderCollectionSetDataToAllIsCalled($orderCollectionMock); + + $this->whenOrderCollectionSaveIsCalled($orderCollectionMock); + + $this->orderCustomerEmailObserver->execute($this->observerMock); + } + + private function createCustomerMock(): MockObject + { + $customer = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + return $customer; + } + + private function createOrderMock(): MockObject + { + $orderCollectionMock = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + + return $orderCollectionMock; + } + + private function setCustomerToEventMock(MockObject $customer): void + { + $this->eventMock->expects($this->once()) + ->method('getCustomerDataObject') + ->willReturn($customer); + } + + private function setOriginalCustomerToEventMock(MockObject $originalCustomer): void + { + $this->eventMock->expects($this->once()) + ->method('getOrigCustomerDataObject') + ->willReturn($originalCustomer); + } + + private function setCustomerEmail(MockObject $originalCustomer, string $email): void + { + $originalCustomer->expects($this->once()) + ->method('getEmail') + ->willReturn($email); + } + + private function whenOrderRepositoryGetListIsCalled(MockObject $orderCollectionMock): void + { + $searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($searchCriteriaMock); + + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('addFilter') + ->willReturn($this->searchCriteriaBuilderMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('getList') + ->with($searchCriteriaMock) + ->willReturn($orderCollectionMock); + } + + private function whenOrderCollectionSetDataToAllIsCalled(MockObject $orderCollectionMock): void + { + $orderCollectionMock->expects($this->once()) + ->method('setDataToAll') + ->with(OrderInterface::CUSTOMER_EMAIL, self::NEW_CUSTOMER_EMAIL); + } + + private function whenOrderCollectionSaveIsCalled(MockObject $orderCollectionMock): void + { + $orderCollectionMock->expects($this->once()) + ->method('save'); + } + + private function whenOrderRepositoryGetListIsNotCalled(): void + { + $this->searchCriteriaBuilderMock->expects($this->never()) + ->method('addFilter'); + $this->searchCriteriaBuilderMock->expects($this->never()) + ->method('create'); + + $this->orderRepositoryMock->expects($this->never()) + ->method('getList'); + } +} diff --git a/app/code/Magento/Customer/etc/events.xml b/app/code/Magento/Customer/etc/events.xml index 2a724498a0359..0194f91c591f5 100644 --- a/app/code/Magento/Customer/etc/events.xml +++ b/app/code/Magento/Customer/etc/events.xml @@ -16,6 +16,7 @@ <observer name="customer_visitor" instance="Magento\Customer\Observer\Visitor\BindQuoteCreateObserver" /> </event> <event name="customer_save_after_data_object"> + <observer name="upgrade_order_customer_email" instance="Magento\Customer\Observer\UpgradeOrderCustomerEmailObserver"/> <observer name="upgrade_quote_customer_email" instance="Magento\Customer\Observer\UpgradeQuoteCustomerEmailObserver"/> </event> </config> diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index f2457963a5f3d..426df2bbaa128 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -19,4 +19,7 @@ </argument> </arguments> </type> + <type name="Magento\Customer\Api\CustomerRepositoryInterface"> + <plugin name="updateCustomerByIdFromRequest" type="Magento\Customer\Model\Plugin\UpdateCustomer" /> + </type> </config> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml index be201afa8f66c..caa501d48da83 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml @@ -6,6 +6,8 @@ * @var $block \Magento\Customer\Block\Account\Forgotpassword */ +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Customer\Block\Account\Forgotpassword $block */ ?> <form class="form password forget" @@ -32,3 +34,12 @@ </div> </div> </form> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "form-validate" + } + } + } +</script> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml index ef74b0062c023..a1d1a0260672a 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Customer\Block\Form\Login $block */ ?> <div class="block block-customer-login"> @@ -22,13 +24,22 @@ <div class="field email required"> <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> - <input name="login[username]" value="<?= $block->escapeHtmlAttr($block->getUsername()) ?>" <?php if ($block->isAutocompleteDisabled()) : ?> autocomplete="off"<?php endif; ?> id="email" type="email" class="input-text" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}"> + <input name="login[username]" value="<?= $block->escapeHtmlAttr($block->getUsername()) ?>" + <?php if ($block->isAutocompleteDisabled()): ?> autocomplete="off"<?php endif; ?> + id="email" type="email" class="input-text" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" + data-mage-init='{"mage/trim-input":{}}' + data-validate="{required:true, 'validate-email':true}"> </div> </div> <div class="field password required"> <label for="pass" class="label"><span><?= $block->escapeHtml(__('Password')) ?></span></label> <div class="control"> - <input name="login[password]" type="password" <?php if ($block->isAutocompleteDisabled()) : ?> autocomplete="off"<?php endif; ?> class="input-text" id="pass" title="<?= $block->escapeHtmlAttr(__('Password')) ?>" data-validate="{required:true}"> + <input name="login[password]" type="password" + <?php if ($block->isAutocompleteDisabled()): ?> autocomplete="off"<?php endif; ?> + class="input-text" id="pass" + title="<?= $block->escapeHtmlAttr(__('Password')) ?>" + data-validate="{required:true}"> </div> </div> <?= $block->getChildHtml('form_additional_info') ?> @@ -41,3 +52,12 @@ </div> </div> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "login-form" + } + } + } +</script> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml index f7d10f6df1728..e84861b9b5cf6 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml @@ -301,13 +301,6 @@ require([ ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden' <?php endif ?> }).find('input:text').attr('autocomplete', 'off'); - dataForm.submit(function () { - $(this).find(':submit').attr('disabled', 'disabled'); - }); - dataForm.bind("invalid-form.validate", function () { - $(this).find(':submit').prop('disabled', false); - }); - }); </script> <?php if ($block->getShowAddressFields()): ?> @@ -337,6 +330,11 @@ require([ "passwordStrengthIndicator": { "formSelector": "form.form-create-account" } + }, + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "form-validate" + } } } </script> diff --git a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js new file mode 100644 index 0000000000000..b941ec7a254d8 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js @@ -0,0 +1,22 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/mage' +], function ($) { + 'use strict'; + + return function (config) { + var dataForm = $('#' + config.formId); + + dataForm.submit(function () { + $(this).find(':submit').attr('disabled', 'disabled'); + }); + dataForm.bind('invalid-form.validate', function () { + $(this).find(':submit').prop('disabled', false); + }); + }; +}); diff --git a/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php new file mode 100644 index 0000000000000..ef3e86788c43f --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Api; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Interface for customer data validator + */ +interface ValidateCustomerDataInterface +{ + /** + * Validate customer data + * + * @param array $customerData + * @throws GraphQlInputException + */ + public function execute(array $customerData): void; +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php index 3861ce324ea7d..95d68d69d71e7 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php @@ -7,6 +7,9 @@ namespace Magento\CustomerGraphQl\Model\Customer; +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; @@ -27,53 +30,41 @@ class ValidateCustomerData */ private $emailAddressValidator; + /** + * @var ValidateCustomerDataInterface[] + */ + private $validators = []; + /** * ValidateCustomerData constructor. * * @param GetAllowedCustomerAttributes $getAllowedCustomerAttributes * @param EmailAddressValidator $emailAddressValidator + * @param array $validators */ public function __construct( GetAllowedCustomerAttributes $getAllowedCustomerAttributes, - EmailAddressValidator $emailAddressValidator + EmailAddressValidator $emailAddressValidator, + $validators = [] ) { $this->getAllowedCustomerAttributes = $getAllowedCustomerAttributes; $this->emailAddressValidator = $emailAddressValidator; + $this->validators = $validators; } /** * Validate customer data * * @param array $customerData - * - * @return void - * * @throws GraphQlInputException + * @throws LocalizedException + * @throws NoSuchEntityException */ - public function execute(array $customerData): void + public function execute(array $customerData) { - $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); - $errorInput = []; - - foreach ($attributes as $attributeInfo) { - if ($attributeInfo->getIsRequired() - && (!isset($customerData[$attributeInfo->getAttributeCode()]) - || $customerData[$attributeInfo->getAttributeCode()] == '') - ) { - $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); - } - } - - if ($errorInput) { - throw new GraphQlInputException( - __('Required parameters are missing: %1', [implode(', ', $errorInput)]) - ); - } - - if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) { - throw new GraphQlInputException( - __('"%1" is not a valid email address.', $customerData['email']) - ); + /** @var ValidateCustomerDataInterface $validator */ + foreach ($this->validators as $validator) { + $validator->execute($customerData); } } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php new file mode 100644 index 0000000000000..87f8831550f04 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; + +/** + * Validates an email + */ +class ValidateEmail implements ValidateCustomerDataInterface +{ + /** + * @var EmailAddressValidator + */ + private $emailAddressValidator; + + /** + * ValidateEmail constructor. + * + * @param EmailAddressValidator $emailAddressValidator + */ + public function __construct(EmailAddressValidator $emailAddressValidator) + { + $this->emailAddressValidator = $emailAddressValidator; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) { + throw new GraphQlInputException( + __('"%1" is not a valid email address.', $customerData['email']) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php new file mode 100644 index 0000000000000..463372a63d8d5 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\Data\AttributeMetadata; +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Validates gender value + */ +class ValidateGender implements ValidateCustomerDataInterface +{ + /** + * @var CustomerMetadataInterface + */ + private $customerMetadata; + + /** + * ValidateGender constructor. + * + * @param CustomerMetadataInterface $customerMetadata + */ + public function __construct(CustomerMetadataInterface $customerMetadata) + { + $this->customerMetadata = $customerMetadata; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + if (isset($customerData['gender']) && $customerData['gender']) { + /** @var AttributeMetadata $genderData */ + $options = $this->customerMetadata->getAttributeMetadata('gender')->getOptions(); + + $isValid = false; + foreach ($options as $optionData) { + if ($optionData->getValue() && $optionData->getValue() == $customerData['gender']) { + $isValid = true; + } + } + + if (!$isValid) { + throw new GraphQlInputException( + __('"%1" is not a valid gender value.', $customerData['gender']) + ); + } + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php new file mode 100644 index 0000000000000..fdf4fa1c824f2 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\CustomerGraphQl\Model\Customer\GetAllowedCustomerAttributes; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Validates required attributes + */ +class ValidateRequiredArguments implements ValidateCustomerDataInterface +{ + /** + * Get allowed/required customer attributes + * + * @var GetAllowedCustomerAttributes + */ + private $getAllowedCustomerAttributes; + + /** + * ValidateRequiredArguments constructor. + * + * @param GetAllowedCustomerAttributes $getAllowedCustomerAttributes + */ + public function __construct(GetAllowedCustomerAttributes $getAllowedCustomerAttributes) + { + $this->getAllowedCustomerAttributes = $getAllowedCustomerAttributes; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); + $errorInput = []; + + foreach ($attributes as $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (!isset($customerData[$attributeInfo->getAttributeCode()]) + || $customerData[$attributeInfo->getAttributeCode()] == '') + ) { + $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index 1ba0e457430e0..3ed77a2ad563c 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -29,4 +29,14 @@ </argument> </arguments> </type> + <!-- Validate input customer data --> + <type name="Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="validateRequiredArguments" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateRequiredArguments</item> + <item name="validateEmail" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateEmail</item> + <item name="validateGender" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateGender</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Developer/Console/Command/patch_template.php.dist b/app/code/Magento/Developer/Console/Command/patch_template.php.dist index f4fc25abcb29a..8e14b24bdc933 100644 --- a/app/code/Magento/Developer/Console/Command/patch_template.php.dist +++ b/app/code/Magento/Developer/Console/Command/patch_template.php.dist @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace %moduleName%\Setup\Patch\%patchType%; @@ -36,7 +37,7 @@ class %class% implements %implementsInterfaces% } %revertFunction% /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { @@ -44,12 +45,10 @@ class %class% implements %implementsInterfaces% } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { - return [ - - ]; + return []; } } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php index ba98524bb665e..fc659c773c0af 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php @@ -21,11 +21,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug */ private $state; - /** - * @var ScopeConfigInterface - */ - private $scopeConfig; - /** * @var DeploymentConfig */ @@ -34,7 +29,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug /** * @param DriverInterface $filesystem * @param State $state - * @param ScopeConfigInterface $scopeConfig * @param DeploymentConfig $deploymentConfig * @param string $filePath * @throws \Exception @@ -42,14 +36,12 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug public function __construct( DriverInterface $filesystem, State $state, - ScopeConfigInterface $scopeConfig, DeploymentConfig $deploymentConfig, $filePath = null ) { parent::__construct($filesystem, $filePath); $this->state = $state; - $this->scopeConfig = $scopeConfig; $this->deploymentConfig = $deploymentConfig; } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php index 3f5ff58640313..c6ee70fb9ce40 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php @@ -29,13 +29,10 @@ class Syslog extends \Magento\Framework\Logger\Handler\Syslog private $deploymentConfig; /** - * @param ScopeConfigInterface $scopeConfig Scope config * @param DeploymentConfig $deploymentConfig Deployment config * @param string $ident The string ident to be added to each message - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - ScopeConfigInterface $scopeConfig, DeploymentConfig $deploymentConfig, string $ident ) { diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php index 8bb0b1f176313..5e824e43764de 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php @@ -44,7 +44,6 @@ protected function setUp(): void $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); $this->model = new Syslog( - $this->scopeConfigMock, $this->deploymentConfigMock, 'Magento' ); diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php index c0bc825a8285b..c449f8f54872f 100644 --- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php +++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php @@ -7,8 +7,9 @@ namespace Magento\Downloadable\Controller\Download; -use Magento\Catalog\Model\Product\SalabilityChecker; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Downloadable\Model\Link as LinkModel; +use Magento\Downloadable\Model\RelatedProductRetriever; use Magento\Framework\App\Action\Context; use Magento\Framework\App\ResponseInterface; @@ -20,20 +21,21 @@ class LinkSample extends \Magento\Downloadable\Controller\Download { /** - * @var SalabilityChecker + * @var RelatedProductRetriever */ - private $salabilityChecker; + private $relatedProductRetriever; /** * @param Context $context - * @param SalabilityChecker|null $salabilityChecker + * @param RelatedProductRetriever $relatedProductRetriever */ public function __construct( Context $context, - SalabilityChecker $salabilityChecker = null + RelatedProductRetriever $relatedProductRetriever ) { parent::__construct($context); - $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class); + + $this->relatedProductRetriever = $relatedProductRetriever; } /** @@ -44,9 +46,10 @@ public function __construct( public function execute() { $linkId = $this->getRequest()->getParam('link_id', 0); - /** @var \Magento\Downloadable\Model\Link $link */ - $link = $this->_objectManager->create(\Magento\Downloadable\Model\Link::class)->load($linkId); - if ($link->getId() && $this->salabilityChecker->isSalable($link->getProductId())) { + /** @var LinkModel $link */ + $link = $this->_objectManager->create(LinkModel::class); + $link->load($linkId); + if ($link->getId() && $this->isProductSalable($link)) { $resource = ''; $resourceType = ''; if ($link->getSampleType() == DownloadHelper::LINK_TYPE_URL) { @@ -74,4 +77,16 @@ public function execute() return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } + + /** + * Check is related product salable. + * + * @param LinkModel $link + * @return bool + */ + private function isProductSalable(LinkModel $link): bool + { + $product = $this->relatedProductRetriever->getProduct((int) $link->getProductId()); + return $product ? $product->isSalable() : false; + } } diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php index b95ec510fdd9b..e2561092a7592 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Sample.php +++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php @@ -7,8 +7,9 @@ namespace Magento\Downloadable\Controller\Download; -use Magento\Catalog\Model\Product\SalabilityChecker; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Downloadable\Model\RelatedProductRetriever; +use Magento\Downloadable\Model\Sample as SampleModel; use Magento\Framework\App\Action\Context; use Magento\Framework\App\ResponseInterface; @@ -20,20 +21,21 @@ class Sample extends \Magento\Downloadable\Controller\Download { /** - * @var SalabilityChecker + * @var RelatedProductRetriever */ - private $salabilityChecker; + private $relatedProductRetriever; /** * @param Context $context - * @param SalabilityChecker|null $salabilityChecker + * @param RelatedProductRetriever $relatedProductRetriever */ public function __construct( Context $context, - SalabilityChecker $salabilityChecker = null + RelatedProductRetriever $relatedProductRetriever ) { parent::__construct($context); - $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class); + + $this->relatedProductRetriever = $relatedProductRetriever; } /** @@ -44,9 +46,10 @@ public function __construct( public function execute() { $sampleId = $this->getRequest()->getParam('sample_id', 0); - /** @var \Magento\Downloadable\Model\Sample $sample */ - $sample = $this->_objectManager->create(\Magento\Downloadable\Model\Sample::class)->load($sampleId); - if ($sample->getId() && $this->salabilityChecker->isSalable($sample->getProductId())) { + /** @var SampleModel $sample */ + $sample = $this->_objectManager->create(SampleModel::class); + $sample->load($sampleId); + if ($sample->getId() && $this->isProductSalable($sample)) { $resource = ''; $resourceType = ''; if ($sample->getSampleType() == DownloadHelper::LINK_TYPE_URL) { @@ -71,4 +74,16 @@ public function execute() return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } + + /** + * Check is related product salable. + * + * @param SampleModel $sample + * @return bool + */ + private function isProductSalable(SampleModel $sample): bool + { + $product = $this->relatedProductRetriever->getProduct((int) $sample->getProductId()); + return $product ? $product->isSalable() : false; + } } diff --git a/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php new file mode 100644 index 0000000000000..f701f96b910e7 --- /dev/null +++ b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Downloadable\Model; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Related parent product retriever. + */ +class RelatedProductRetriever +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @param ProductRepositoryInterface $productRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param MetadataPool $metadataPool + */ + public function __construct( + ProductRepositoryInterface $productRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + MetadataPool $metadataPool + ) { + $this->productRepository = $productRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->metadataPool = $metadataPool; + } + + /** + * Get related product. + * + * @param int $productId + * @return ProductInterface|null + */ + public function getProduct(int $productId): ?ProductInterface + { + $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $searchCriteria = $this->searchCriteriaBuilder->addFilter($productMetadata->getLinkField(), $productId) + ->create(); + $items = $this->productRepository->getList($searchCriteria) + ->getItems(); + $product = $items ? array_shift($items) : null; + + return $product; + } +} diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php index 8d30322745b8d..b7b079d208d97 100644 --- a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php +++ b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php @@ -24,7 +24,7 @@ class Sample extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool - * @param null $connectionName + * @param string|null $connectionName */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -126,7 +126,7 @@ public function getSearchableData($productId, $storeId) )->join( ['cpe' => $this->getTable('catalog_product_entity')], sprintf( - 'cpe.entity_id = m.product_id', + 'cpe.%s = m.product_id', $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() ), [] diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml index 9ad20385519d1..34b9701f2dca5 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml @@ -37,7 +37,7 @@ <argument name="link" value="downloadableLink"/> <argument name="index" value="0"/> </actionGroup> - <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductAfterAddingDomainToWhitelist" after="addDownloadableProductLinkAgain" /> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductAfterAddingDomainToAllowlist" after="addDownloadableProductLinkAgain" /> <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="scrollToLinks"/> <click selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="selectProductLink"/> <see selector="{{CheckoutCartProductSection.ProductPriceByName(DownloadableProduct.name)}}" userInput="$52.99" stepKey="assertProductPriceInCart"/> diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php deleted file mode 100644 index 725c06004f117..0000000000000 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php +++ /dev/null @@ -1,237 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Downloadable\Test\Unit\Controller\Download; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\SalabilityChecker; -use Magento\Downloadable\Controller\Download\LinkSample; -use Magento\Downloadable\Helper\Data; -use Magento\Downloadable\Helper\Download; -use Magento\Downloadable\Helper\File; -use Magento\Downloadable\Model\Link; -use Magento\Framework\App\Request\Http; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\App\Response\RedirectInterface; -use Magento\Framework\App\ResponseInterface; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Framework\UrlInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Unit tests for \Magento\Downloadable\Controller\Download\LinkSample. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class LinkSampleTest extends TestCase -{ - /** @var LinkSample */ - protected $linkSample; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** - * @var MockObject|Http - */ - protected $request; - - /** - * @var MockObject|ResponseInterface - */ - protected $response; - - /** - * @var MockObject|\Magento\Framework\ObjectManager\ObjectManager - */ - protected $objectManager; - - /** - * @var MockObject|ManagerInterface - */ - protected $messageManager; - - /** - * @var MockObject|RedirectInterface - */ - protected $redirect; - - /** - * @var MockObject|Data - */ - protected $helperData; - - /** - * @var MockObject|\Magento\Downloadable\Helper\Download - */ - protected $downloadHelper; - - /** - * @var MockObject|Product - */ - protected $product; - - /** - * @var MockObject|UrlInterface - */ - protected $urlInterface; - - /** - * @var SalabilityChecker|MockObject - */ - private $salabilityCheckerMock; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp(): void - { - $this->objectManagerHelper = new ObjectManagerHelper($this); - - $this->request = $this->getMockForAbstractClass(RequestInterface::class); - $this->response = $this->getMockBuilder(ResponseInterface::class) - ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect']) - ->onlyMethods(['sendResponse']) - ->getMockForAbstractClass(); - - $this->helperData = $this->createPartialMock( - Data::class, - ['getIsShareable'] - ); - $this->downloadHelper = $this->createPartialMock( - Download::class, - [ - 'setResource', - 'getFilename', - 'getContentType', - 'getFileSize', - 'getContentDisposition', - 'output' - ] - ); - $this->product = $this->getMockBuilder(Product::class) - ->addMethods(['_wakeup']) - ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); - $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class); - $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class); - $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->linkSample = $this->objectManagerHelper->getObject( - LinkSample::class, - [ - 'objectManager' => $this->objectManager, - 'request' => $this->request, - 'response' => $this->response, - 'messageManager' => $this->messageManager, - 'redirect' => $this->redirect, - 'salabilityChecker' => $this->salabilityCheckerMock, - ] - ); - } - - /** - * Execute Download link's sample action with Url link. - * - * @return void - */ - public function testExecuteLinkTypeUrl() - { - $linkMock = $this->getMockBuilder(Link::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id'); - $this->objectManager->expects($this->once()) - ->method('create') - ->with(Link::class) - ->willReturn($linkMock); - $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf(); - $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id'); - $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); - $linkMock->expects($this->once())->method('getSampleType')->willReturn( - Download::LINK_TYPE_URL - ); - $linkMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url'); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->linkSample->execute()); - } - - /** - * Execute Download link's sample action with File link. - * - * @return void - */ - public function testExecuteLinkTypeFile() - { - $linkMock = $this->getMockBuilder(Link::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath']) - ->getMock(); - $fileMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['getFilePath', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id'); - $this->objectManager->expects($this->at(0)) - ->method('create') - ->with(Link::class) - ->willReturn($linkMock); - $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf(); - $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id'); - $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); - $linkMock->expects($this->any())->method('getSampleType')->willReturn( - Download::LINK_TYPE_FILE - ); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(File::class) - ->willReturn($fileMock); - $this->objectManager->expects($this->at(2)) - ->method('get') - ->with(Link::class) - ->willReturn($linkMock); - $linkMock->expects($this->once())->method('getBaseSamplePath')->willReturn('downloadable/files/link_samples'); - $this->objectManager->expects($this->at(3)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->linkSample->execute()); - } -} diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php deleted file mode 100644 index 6dcd09a91dd2e..0000000000000 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php +++ /dev/null @@ -1,232 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Downloadable\Test\Unit\Controller\Download; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\SalabilityChecker; -use Magento\Downloadable\Controller\Download\Sample; -use Magento\Downloadable\Helper\Data; -use Magento\Downloadable\Helper\Download; -use Magento\Downloadable\Helper\File; -use Magento\Framework\App\Request\Http; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\App\Response\RedirectInterface; -use Magento\Framework\App\ResponseInterface; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Framework\UrlInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Unit tests for \Magento\Downloadable\Controller\Download\Sample. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SampleTest extends TestCase -{ - /** @var \Magento\Downloadable\Controller\Download\Sample */ - protected $sample; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** - * @var MockObject|Http - */ - protected $request; - - /** - * @var MockObject|ResponseInterface - */ - protected $response; - - /** - * @var MockObject|\Magento\Framework\ObjectManager\ObjectManager - */ - protected $objectManager; - - /** - * @var MockObject|ManagerInterface - */ - protected $messageManager; - - /** - * @var MockObject|RedirectInterface - */ - protected $redirect; - - /** - * @var MockObject|Data - */ - protected $helperData; - - /** - * @var MockObject|\Magento\Downloadable\Helper\Download - */ - protected $downloadHelper; - - /** - * @var MockObject|Product - */ - protected $product; - - /** - * @var MockObject|UrlInterface - */ - protected $urlInterface; - - /** - * @var SalabilityChecker|MockObject - */ - private $salabilityCheckerMock; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp(): void - { - $this->objectManagerHelper = new ObjectManagerHelper($this); - - $this->request = $this->getMockForAbstractClass(RequestInterface::class); - $this->response = $this->getMockBuilder(ResponseInterface::class) - ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect']) - ->onlyMethods(['sendResponse']) - ->getMockForAbstractClass(); - - $this->helperData = $this->createPartialMock( - Data::class, - ['getIsShareable'] - ); - $this->downloadHelper = $this->createPartialMock( - Download::class, - [ - 'setResource', - 'getFilename', - 'getContentType', - 'getFileSize', - 'getContentDisposition', - 'output' - ] - ); - $this->product = $this->getMockBuilder(Product::class) - ->addMethods(['_wakeup']) - ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); - $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class); - $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class); - $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->sample = $this->objectManagerHelper->getObject( - Sample::class, - [ - 'objectManager' => $this->objectManager, - 'request' => $this->request, - 'response' => $this->response, - 'messageManager' => $this->messageManager, - 'redirect' => $this->redirect, - 'salabilityChecker' => $this->salabilityCheckerMock, - ] - ); - } - - /** - * Execute Download sample action with Sample Url. - * - * @return void - */ - public function testExecuteSampleWithUrlType() - { - $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id'); - $this->objectManager->expects($this->once()) - ->method('create') - ->with(\Magento\Downloadable\Model\Sample::class) - ->willReturn($sampleMock); - $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf(); - $sampleMock->expects($this->once())->method('getId')->willReturn('some_link_id'); - $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); - $sampleMock->expects($this->once())->method('getSampleType')->willReturn( - Download::LINK_TYPE_URL - ); - $sampleMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url'); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->sample->execute()); - } - - /** - * Execute Download sample action with Sample File. - * - * @return void - */ - public function testExecuteSampleWithFileType() - { - $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath']) - ->getMock(); - $fileHelperMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['getFilePath']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id'); - $this->objectManager->expects($this->at(0)) - ->method('create') - ->with(\Magento\Downloadable\Model\Sample::class) - ->willReturn($sampleMock); - $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf(); - $sampleMock->expects($this->once())->method('getId')->willReturn('some_sample_id'); - $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); - $sampleMock->expects($this->any())->method('getSampleType')->willReturn( - Download::LINK_TYPE_FILE - ); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(File::class) - ->willReturn($fileHelperMock); - $fileHelperMock->expects($this->once())->method('getFilePath')->willReturn('file_path'); - $this->objectManager->expects($this->at(2)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->sample->execute()); - } -} diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php index 15dcea077c887..7e434166a15be 100644 --- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php @@ -23,12 +23,12 @@ class Data extends \Magento\Framework\Validator\AbstractValidator /** * @var array */ - protected $_attributesWhiteList = []; + protected $allowedAttributesList = []; /** * @var array */ - protected $_attributesBlackList = []; + protected $deniedAttributesList = []; /** * @var array @@ -68,9 +68,9 @@ public function setAttributes(array $attributes) * @param array $attributesCodes * @return $this */ - public function setAttributesWhiteList(array $attributesCodes) + public function setAllowedAttributesList(array $attributesCodes) { - $this->_attributesWhiteList = $attributesCodes; + $this->allowedAttributesList = $attributesCodes; return $this; } @@ -82,9 +82,9 @@ public function setAttributesWhiteList(array $attributesCodes) * @param array $attributesCodes * @return $this */ - public function setAttributesBlackList(array $attributesCodes) + public function setDeniedAttributesList(array $attributesCodes) { - $this->_attributesBlackList = $attributesCodes; + $this->deniedAttributesList = $attributesCodes; return $this; } @@ -171,11 +171,11 @@ protected function _getAttributes($entity) $attributesCodes[] = $attributeCode; } - $ignoreAttributes = $this->_attributesBlackList; - if ($this->_attributesWhiteList) { + $ignoreAttributes = $this->deniedAttributesList; + if ($this->allowedAttributesList) { $ignoreAttributes = array_merge( $ignoreAttributes, - array_diff($attributesCodes, $this->_attributesWhiteList) + array_diff($attributesCodes, $this->allowedAttributesList) ); } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php index 774b968f1b697..a8ecbb8371ac9 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php @@ -249,10 +249,10 @@ public function testIsValidAttributesFromCollection() } /** - * @dataProvider whiteBlackListProvider + * @dataProvider allowDenyListProvider * @param callable $callback */ - public function testIsValidBlackListWhiteListChecks($callback) + public function testIsValidExclusionInclusionListChecks($callback) { $attribute = $this->_getAttributeMock( [ @@ -302,19 +302,19 @@ public function testIsValidBlackListWhiteListChecks($callback) /** * @return array */ - public function whiteBlackListProvider() + public function allowDenyListProvider() { - $whiteCallback = function ($validator) { - $validator->setAttributesWhiteList(['attribute']); + $allowedCallbackList = function ($validator) { + $validator->setAllowedAttributesList(['attribute']); }; - $blackCallback = function ($validator) { - $validator->setAttributesBlackList(['attribute2']); + $deniedCallbackList = function ($validator) { + $validator->setDeniedAttributesList(['attribute2']); }; - return ['white_list' => [$whiteCallback], 'black_list' => [$blackCallback]]; + return ['allowed' => [$allowedCallbackList], 'denied' => [$deniedCallbackList]]; } - public function testSetAttributesWhiteList() + public function testSetAttributesAllowedList() { $this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties'); @@ -328,12 +328,14 @@ public function testSetAttributesWhiteList() ) ->getMock(); $validator = new Data($attrDataFactory); - $result = $validator->setAttributesWhiteList($attributes); - $this->assertAttributeEquals($attributes, '_attributesWhiteList', $validator); + $result = $validator->setIncludedAttributesList($attributes); + + // phpstan:ignore + $this->assertAttributeEquals($attributes, '_attributesAllowed', $validator); $this->assertEquals($validator, $result); } - public function testSetAttributesBlackList() + public function testSetAttributesDeniedList() { $this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties'); @@ -347,8 +349,9 @@ public function testSetAttributesBlackList() ) ->getMock(); $validator = new Data($attrDataFactory); - $result = $validator->setAttributesBlackList($attributes); - $this->assertAttributeEquals($attributes, '_attributesBlackList', $validator); + $result = $validator->setDeniedAttributesList($attributes); + // phpstan:ignore + $this->assertAttributeEquals($attributes, '_attributesDenied', $validator); $this->assertEquals($validator, $result); } diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php index 1f6e05c9e02fc..8576d8df0cc95 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -19,7 +19,7 @@ class Converter implements ConverterInterface */ private const ES_DATA_TYPE_TEXT = 'text'; private const ES_DATA_TYPE_KEYWORD = 'keyword'; - private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_DOUBLE = 'double'; private const ES_DATA_TYPE_INT = 'integer'; private const ES_DATA_TYPE_DATE = 'date'; /**#@-*/ @@ -32,7 +32,7 @@ class Converter implements ConverterInterface private $mapping = [ self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT, self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, ]; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php index bd9a380230652..8d8787a5eff72 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php @@ -276,7 +276,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php index 88dab83698794..2067dcdc7fe9f 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -16,7 +16,7 @@ class Converter implements ConverterInterface * Text flags for Elasticsearch field types */ private const ES_DATA_TYPE_STRING = 'string'; - private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_DOUBLE = 'double'; private const ES_DATA_TYPE_INT = 'integer'; private const ES_DATA_TYPE_DATE = 'date'; /**#@-*/ @@ -29,7 +29,7 @@ class Converter implements ConverterInterface private $mapping = [ self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_STRING, self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_STRING, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, ]; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php index 1e106023ea00d..548a57e55f3e2 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php @@ -35,7 +35,7 @@ public function __construct(Repository $algorithmRepository, EntityStorageFactor } /** - * {@inheritdoc} + * @inheritdoc */ public function build( RequestBucketInterface $bucket, @@ -46,9 +46,7 @@ public function build( /** @var DynamicBucket $bucket */ $algorithm = $this->algorithmRepository->get($bucket->getMethod(), ['dataProvider' => $dataProvider]); $data = $algorithm->getItems($bucket, $dimensions, $this->getEntityStorage($queryResult)); - $resultData = $this->prepareData($data); - - return $resultData; + return $this->prepareData($data); } /** @@ -77,12 +75,9 @@ private function prepareData($data) { $resultData = []; foreach ($data as $value) { - $from = is_numeric($value['from']) ? $value['from'] : '*'; - $to = is_numeric($value['to']) ? $value['to'] : '*'; - unset($value['from'], $value['to']); - - $rangeName = "{$from}_{$to}"; - $resultData[$rangeName] = array_merge(['value' => $rangeName], $value); + $rangeName = "{$value['from']}_{$value['to']}"; + $value['value'] = $rangeName; + $resultData[$rangeName] = $value; } return $resultData; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php index 496a77e4c5ac3..7bc64b59ffe78 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php @@ -235,11 +235,9 @@ public function prepareData($range, array $dbRanges) { $data = []; if (!empty($dbRanges)) { - $lastIndex = array_keys($dbRanges); - $lastIndex = $lastIndex[count($lastIndex) - 1]; foreach ($dbRanges as $index => $count) { - $fromPrice = $index == 1 ? '' : ($index - 1) * $range; - $toPrice = $index == $lastIndex ? '' : $index * $range; + $fromPrice = $index == 1 ? 0 : ($index - 1) * $range; + $toPrice = $index * $range; $data[] = [ 'from' => $fromPrice, 'to' => $toPrice, diff --git a/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php new file mode 100644 index 0000000000000..7cd72c322d647 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Elasticsearch\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Framework\Setup\Patch\PatchInterface; + +/** + * Invalidate fulltext index + */ +class InvalidateIndex implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param IndexerRegistry $indexerRegistry + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup, IndexerRegistry $indexerRegistry) + { + $this->moduleDataSetup = $moduleDataSetup; + $this->indexerRegistry = $indexerRegistry; + } + + /** + * @inheritDoc + */ + public function apply(): PatchInterface + { + $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID)->invalidate(); + return $this; + } + + /** + * @inheritDoc + */ + public static function getDependencies(): array + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases(): array + { + return []; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php index 49a894f1295c7..575a64dc43abd 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php @@ -329,7 +329,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -400,7 +400,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index 87f072836544e..a9bcd1a20a1b2 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -246,7 +246,7 @@ function ($type) use ($complexType) { if ($type === 'string') { return 'string'; } elseif ($type === 'float') { - return 'float'; + return 'double'; } elseif ($type === 'integer') { return 'integer'; } else { @@ -281,7 +281,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ] @@ -300,7 +300,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ], @@ -319,7 +319,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ] diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php index 75b79bc43e805..718adf255254f 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php @@ -56,7 +56,7 @@ public function convertProvider() { return [ ['string', 'string'], - ['float', 'float'], + ['float', 'double'], ['integer', 'integer'], ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php index c5b9089acd91c..0595b667f4ee8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php @@ -390,13 +390,13 @@ public function testPrepareData() { $expectedResult = [ [ - 'from' => '', + 'from' => 0, 'to' => 10, 'count' => 1, ], [ 'from' => 10, - 'to' => '', + 'to' => 20, 'count' => 1, ], ]; diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 633889e70591b..633e67dfe698e 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -537,7 +537,7 @@ </type> <type name="Magento\Search\Model\SearchEngine\Validator"> <arguments> - <argument name="engineBlacklist" xsi:type="array"> + <argument name="excludedEngineList" xsi:type="array"> <item name="elasticsearch" xsi:type="string">Elasticsearch 2</item> </argument> <argument name="engineValidators" xsi:type="array"> diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php index 2c1c283c5b24d..0571b075aff28 100644 --- a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php @@ -281,7 +281,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php index aa0b49400c517..2a7fa2ce8114a 100644 --- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php @@ -439,7 +439,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -509,7 +509,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php index feacca8d62804..4b318f987abfe 100644 --- a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php @@ -289,7 +289,7 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php index 593bbd7792f46..091387f844d55 100644 --- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php @@ -438,7 +438,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -509,7 +509,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl index 62795f07239a6..3629bb424f207 100644 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl @@ -472,7 +472,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -983,7 +983,7 @@ </xs:element> <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions</xs:documentation> + <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions</xs:documentation> </xs:annotation> </xs:element> <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> @@ -1005,7 +1005,7 @@ <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="99"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -4867,4 +4867,4 @@ <s1:address location="https://wsbeta.fedex.com:443/web-services/rate"/> </port> </service> -</definitions> \ No newline at end of file +</definitions> diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl index 17a6f74cc09b8..2f3feecb58084 100644 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl @@ -471,7 +471,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment commitment more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -983,7 +983,7 @@ </xs:element> <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions</xs:documentation> + <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions</xs:documentation> </xs:annotation> </xs:element> <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> @@ -1005,7 +1005,7 @@ <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="99"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl index 54bb57d490c76..439d032a61fd0 100644 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl @@ -497,7 +497,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -724,7 +724,7 @@ </xs:element> <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> <xs:annotation> - <xs:documentation>The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> + <xs:documentation>The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl index d8dc0fdfed4ab..a449bf41dbd68 100644 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl @@ -497,7 +497,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -724,7 +724,7 @@ </xs:element> <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> <xs:annotation> - <xs:documentation>The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> + <xs:documentation>The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php index c317221fb6ef7..2ce51c8bbf19d 100644 --- a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php @@ -66,7 +66,7 @@ public function resolve( array $args = null ) { if (!isset($value['model'])) { - throw new GraphQlInputException(__('"model" value should be specified')); + throw new GraphQlInputException(__('"model" value must be specified')); } $cart = $value['model']; diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php new file mode 100644 index 0000000000000..a9a8e682612cc --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessageGraphQl\Model\Resolver\Cart\Item; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GiftMessage\Api\ItemRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; + +/** + * Class provides ability to get GiftMessage for cart item + */ +class GiftMessage implements ResolverInterface +{ + /** + * @var ItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @param ItemRepositoryInterface $itemRepository + * @param GiftMessageHelper $giftMessageHelper + */ + public function __construct( + ItemRepositoryInterface $itemRepository, + GiftMessageHelper $giftMessageHelper + ) { + $this->itemRepository = $itemRepository; + $this->giftMessageHelper = $giftMessageHelper; + } + + /** + * Return information about Gift message for item cart + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('"model" value must be specified')); + } + + $quoteItem = $value['model']; + + if (!$this->giftMessageHelper->isMessagesAllowed('items', $quoteItem)) { + return null; + } + + if (!$this->giftMessageHelper->isMessagesAllowed('item', $quoteItem)) { + return null; + } + + try { + $giftItemMessage = $this->itemRepository->get($quoteItem->getQuoteId(), $quoteItem->getItemId()); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Can\'t load cart item')); + } + + if (!isset($giftItemMessage)) { + return null; + } + + return [ + 'to' => $giftItemMessage->getRecipient() ?? '', + 'from' => $giftItemMessage->getSender() ?? '', + 'message'=> $giftItemMessage->getMessage() ?? '' + ]; + } +} diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md index fa2e02116b66c..d73a058e0db9c 100644 --- a/app/code/Magento/GiftMessageGraphQl/README.md +++ b/app/code/Magento/GiftMessageGraphQl/README.md @@ -1,3 +1,3 @@ # GiftMessageGraphQl -**GiftMessageGraphQl** provides information about gift messages for cart, cart items, order and order items. +**GiftMessageGraphQl** provides information about gift messages for carts, cart items, orders and order items. diff --git a/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..bce5b7063e6b9 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml @@ -0,0 +1,18 @@ +<?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:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="allow_order" xsi:type="string">sales/gift_options/allow_order</item> + <item name="allow_items" xsi:type="string">sales/gift_options/allow_items</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls index f14c812a9a5f3..ad18054abca13 100644 --- a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls +++ b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls @@ -1,20 +1,47 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type StoreConfig { + allow_order : String @doc(description: "The value of the Allow Gift Messages on Order Level option") + allow_items : String @doc(description: "The value of the Allow Gift Messages for Order Items option") +} + type Cart { gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\GiftMessage") @doc(description: "The entered gift message for the cart") } -type SalesItemInterface { - gift_message: GiftMessage @doc(description: "The entered gift message for the order item") +type SimpleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") } -type CustomerOrder { - gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order") +type ConfigurableCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type BundleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type GiftMessage @doc(description: "Contains the text of a gift message, its sender, and recipient") { + to: String! @doc(description: "Recipient name") + from: String! @doc(description: "Sender name") + message: String! @doc(description: "Gift message text") +} + +input CartItemUpdateInput { + gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item") } -type GiftMessage { - to: String! @doc(description: "Recepient name") +input GiftMessageInput @doc(description: "Contains the text of a gift message, its sender, and recipient") { + to: String! @doc(description: "Recipient name") from: String! @doc(description: "Sender name") message: String! @doc(description: "Gift message text") } + +type SalesItemInterface { + gift_message: GiftMessage @doc(description: "The entered gift message for the order item") +} + +type CustomerOrder { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order") +} diff --git a/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php b/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php index 135c8c92c6aa9..975788abe52e4 100644 --- a/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php +++ b/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php @@ -5,12 +5,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\GoogleOptimizer\Observer; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; /** + * Abstract entity for saving codes + * * @api * @since 100.0.2 */ @@ -96,7 +100,9 @@ protected function _processCode() $this->_initRequestParams(); if ($this->_isNewCode()) { - $this->_saveCode(); + if (!$this->_isEmptyCode()) { + $this->_saveCode(); + } } else { $this->_loadCode(); if ($this->_isEmptyCode()) { @@ -185,6 +191,8 @@ protected function _deleteCode() } /** + * Check data availability + * * @return bool */ private function isDataAvailable() @@ -194,6 +202,8 @@ private function isDataAvailable() } /** + * Get request data + * * @return mixed */ private function getRequestData() diff --git a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php index 8a5c247369657..c6d02957c4be9 100644 --- a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php +++ b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php @@ -127,6 +127,39 @@ public function testCreatingCodeIfRequestIsValid() $this->_modelObserver->execute($this->_eventObserverMock); } + /** + * Test that code is not saving when request is empty + * + * @return void + */ + public function testCreatingCodeIfRequestIsEmpty(): void + { + $this->_helperMock->expects( + $this->once() + )->method( + 'isGoogleExperimentActive' + )->with( + $this->_storeId + )->willReturn( + true + ); + + $this->_requestMock->expects( + $this->exactly(3) + )->method( + 'getParam' + )->with( + 'google_experiment' + )->willReturn( + ['code_id' => '', 'experiment_script' => ''] + ); + + $this->_codeMock->expects($this->never())->method('addData'); + $this->_codeMock->expects($this->never())->method('save'); + + $this->_modelObserver->execute($this->_eventObserverMock); + } + /** * @param array $params * @dataProvider dataProviderWrongRequestForCreating diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php new file mode 100644 index 0000000000000..ba2e995d4f704 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Credentials header if CORS is enabled + */ +class CorsAllowCredentialsHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Get value for header + * + * @return string + */ + public function getValue(): string + { + return "1"; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->corsConfiguration->isCredentialsAllowed(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php new file mode 100644 index 0000000000000..68760de543daa --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Headers header if CORS is enabled + */ +class CorsAllowHeadersHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedHeaders(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php new file mode 100644 index 0000000000000..233839b9deb74 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Methods header if CORS is enabled + */ +class CorsAllowMethodsHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedMethods(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php new file mode 100644 index 0000000000000..21850f18db1f2 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Origin header if CORS is enabled + */ +class CorsAllowOriginHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedOrigins(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php new file mode 100644 index 0000000000000..e30209ae25e68 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Max-Age header if CORS is enabled + */ +class CorsMaxAgeHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return (string) $this->corsConfiguration->getMaxAge(); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/Configuration.php b/app/code/Magento/GraphQl/Model/Cors/Configuration.php new file mode 100644 index 0000000000000..dd5a0b426e22d --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/Configuration.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Cors; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Configuration provider for GraphQL CORS settings + */ +class Configuration implements ConfigurationInterface +{ + public const XML_PATH_CORS_HEADERS_ENABLED = 'graphql/cors/enabled'; + public const XML_PATH_CORS_ALLOWED_ORIGINS = 'graphql/cors/allowed_origins'; + public const XML_PATH_CORS_ALLOWED_HEADERS = 'graphql/cors/allowed_headers'; + public const XML_PATH_CORS_ALLOWED_METHODS = 'graphql/cors/allowed_methods'; + public const XML_PATH_CORS_MAX_AGE = 'graphql/cors/max_age'; + public const XML_PATH_CORS_ALLOW_CREDENTIALS = 'graphql/cors/allow_credentials'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_HEADERS_ENABLED); + } + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_ORIGINS); + } + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_HEADERS); + } + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_METHODS); + } + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int + { + return (int) $this->scopeConfig->getValue(self::XML_PATH_CORS_MAX_AGE); + } + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_ALLOW_CREDENTIALS); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php new file mode 100644 index 0000000000000..b40b64f48e51f --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Cors; + +/** + * Interface for configuration provider for GraphQL CORS settings + */ +interface ConfigurationInterface +{ + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool; + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string; + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string; + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string; + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int; + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed() : bool; +} diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json index 904d41c97953e..401e77a787acf 100644 --- a/app/code/Magento/GraphQl/composer.json +++ b/app/code/Magento/GraphQl/composer.json @@ -5,10 +5,10 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/module-eav": "*", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-webapi": "*" }, "suggest": { - "magento/module-webapi": "*", "magento/module-graph-ql-cache": "*" }, "license": [ diff --git a/app/code/Magento/GraphQl/etc/adminhtml/system.xml b/app/code/Magento/GraphQl/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..ddee7596eca3e --- /dev/null +++ b/app/code/Magento/GraphQl/etc/adminhtml/system.xml @@ -0,0 +1,65 @@ +<?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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="graphql" translate="label" type="text" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>GraphQL</label> + <tab>service</tab> + <resource>Magento_Integration::config_oauth</resource> + <group id="cors" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="1"> + <label>CORS Settings</label> + <field id="enabled" translate="label" type="select" sortOrder="1" showInDefault="1" canRestore="1"> + <label>CORS Headers Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + + <field id="allowed_origins" translate="label" type="text" sortOrder="10" showInDefault="1" canRestore="1"> + <label>Allowed origins</label> + <comment>The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Fill this field with one or more origins (comma separated) or use '*' to allow access from all origins.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allowed_methods" translate="label" type="text" sortOrder="20" showInDefault="1" canRestore="1"> + <label>Allowed methods</label> + <comment>The Access-Control-Allow-Methods response header specifies the method or methods allowed when accessing the resource in response to a preflight request. Use comma separated methods (e.g. GET,POST)</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allowed_headers" translate="label" type="text" sortOrder="30" showInDefault="1" canRestore="1"> + <label>Allowed headers</label> + <comment>The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request. Use comma separated headers.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="max_age" translate="label" type="text" sortOrder="40" showInDefault="1" canRestore="1"> + <label>Max Age</label> + <validate>validate-digits</validate> + <comment>The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allow_credentials" translate="label" type="select" sortOrder="50" showInDefault="1" canRestore="1"> + <label>Credentials Allowed</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend code when the request's credentials mode is include.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/GraphQl/etc/config.xml b/app/code/Magento/GraphQl/etc/config.xml new file mode 100644 index 0000000000000..39caacbec42d2 --- /dev/null +++ b/app/code/Magento/GraphQl/etc/config.xml @@ -0,0 +1,22 @@ +<?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:module:Magento_Store:etc/config.xsd"> + <default> + <graphql> + <cors> + <enabled>0</enabled> + <allowed_origins></allowed_origins> + <allowed_methods></allowed_methods> + <allowed_headers></allowed_headers> + <max_age>86400</max_age> + <allow_credentials>0</allow_credentials> + </cors> + </graphql> + </default> +</config> diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index b356f33c4f4bf..fca6c425e2507 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -98,4 +98,31 @@ <argument name="queryComplexity" xsi:type="number">300</argument> </arguments> </type> + + <preference for="Magento\GraphQl\Model\Cors\ConfigurationInterface" type="Magento\GraphQl\Model\Cors\Configuration" /> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Max-Age</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Credentials</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Headers</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Methods</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Origin</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml index 77fce336374dd..23d49124d1a02 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -30,4 +30,15 @@ </argument> </arguments> </type> + <type name="Magento\Framework\App\Response\HeaderManager"> + <arguments> + <argument name="headerProviderList" xsi:type="array"> + <item name="CorsAllowOrigins" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider</item> + <item name="CorsAllowHeaders" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider</item> + <item name="CorsAllowMethods" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider</item> + <item name="CorsAllowCredentials" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider</item> + <item name="CorsMaxAge" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index fccde015c3388..0212d32db0f2f 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -79,6 +79,12 @@ input FilterMatchTypeInput @doc(description: "Defines a filter that performs a f match: String @doc(description: "One or more words to filter on") } +input FilterStringTypeInput @doc(description: "Defines a filter for an input string.") { + in: [String] @doc(description: "Filters items that are exactly the same as entries specified in an array of strings.") + eq: String @doc(description: "Filters items that are exactly the same as the specified string.") + match: String @doc(description: "Defines a filter that performs a fuzzy search using the specified string.") +} + type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { page_size: Int @doc(description: "Specifies the maximum number of items to return") current_page: Int @doc(description: "Specifies which page of results to return") diff --git a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php index 92cfb375fea41..29fa2bffabb3b 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class GroupedProductLinksTypeResolver implements TypeResolverInterface { @@ -20,14 +20,14 @@ class GroupedProductLinksTypeResolver implements TypeResolverInterface private $linkTypes = ['associated']; /** - * {@inheritdoc} + * @inheritdoc */ - public function resolveType(array $data) : string + public function resolveType(array $data): string { if (isset($data['link_type'])) { $linkType = $data['link_type']; if (in_array($linkType, $this->linkTypes)) { - return 'GroupedProductLinks'; + return 'ProductLinks'; } } return ''; diff --git a/app/code/Magento/GroupedProductGraphQl/etc/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/di.xml index 35b63370baf2f..717bc14826f70 100644 --- a/app/code/Magento/GroupedProductGraphQl/etc/di.xml +++ b/app/code/Magento/GroupedProductGraphQl/etc/di.xml @@ -13,4 +13,11 @@ </argument> </arguments> </type> + <type name="\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks"> + <arguments> + <argument name="linkTypes" xsi:type="array"> + <item name="associated" xsi:type="string">associated</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php index 09b17371ae4e8..f5b62df9aea2c 100644 --- a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php +++ b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php @@ -54,6 +54,19 @@ public function destruct() { if (is_object($this->_fileHandler)) { $this->_fileHandler->close(); + $this->resolveDestination(); + } + } + + /** + * Remove temporary destination + * + * @return void + */ + private function resolveDestination(): void + { + // only temporary file located directly in var folder + if (strpos($this->_destination, '/') === false) { $this->_directoryHandle->delete($this->_destination); } } diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php index 5bd956c1bc322..9bf5b945c8fbd 100644 --- a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php @@ -15,6 +15,7 @@ /** * Import entity abstract model * + * phpcs:disable Magento2.Classes.AbstractApi * @api * * @SuppressWarnings(PHPMD.TooManyFields) @@ -335,6 +336,8 @@ public function __construct( } /** + * Returns Error aggregator + * * @return ProcessingErrorAggregatorInterface */ public function getErrorAggregator() @@ -413,7 +416,7 @@ protected function _saveValidatedBunches() $source->rewind(); $this->_dataSourceModel->cleanBunches(); - $masterAttributeCode = $this->getMasterAttributeCode(); + $mainAttributeCode = $this->getMasterAttributeCode(); while ($source->valid() || count($bunchRows) || isset($entityGroup)) { if ($startNewBunch || !$source->valid()) { @@ -453,7 +456,7 @@ protected function _saveValidatedBunches() continue; } - if (isset($rowData[$masterAttributeCode]) && trim($rowData[$masterAttributeCode])) { + if (isset($rowData[$mainAttributeCode]) && trim($rowData[$mainAttributeCode])) { /* Add entity group that passed validation to bunch */ if (isset($entityGroup)) { foreach ($entityGroup as $key => $value) { @@ -590,6 +593,7 @@ public function getBehavior(array $rowData = null) * Get default import behavior * * @return string + * phpcs:disable Magento2.Functions.StaticFunction */ public static function getDefaultBehavior() { @@ -652,7 +656,9 @@ public function isAttributeParticular($attributeCode) } /** - * @return string the master attribute code to use in an import + * Returns the master attribute code to use in an import + * + * @return string */ public function getMasterAttributeCode() { diff --git a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php new file mode 100644 index 0000000000000..1565d455cc43f --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Observer; + +use Magento\Catalog\Model\Category as CatalogCategory; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the catalog_category_delete_after event and deletes relation between category content and media asset. + */ +class CategoryDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'catalog_category'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $category = $observer->getEvent()->getData('category'); + $contentAssetLinks = []; + + if ($category instanceof CatalogCategory) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $category->getEntityId(), + ] + ); + $content = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $assets = $this->extractAssetsFromContent->execute($content); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php new file mode 100644 index 0000000000000..421bb5a33fa1d --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Observer; + +use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the catalog_product_delete_before event and deletes relation between category content and media asset. + */ +class ProductDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'catalog_product'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted product and remove relation betwen product and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $product = $observer->getEvent()->getData('product'); + $contentAssetLinks = []; + + if ($product instanceof CatalogProduct) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $product->getEntityId(), + ] + ); + $productContent = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $assets = $this->extractAssetsFromContent->execute($productContent); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCatalog/etc/di.xml b/app/code/Magento/MediaContentCatalog/etc/di.xml index a2d300a2bb208..6b0ee83b30788 100644 --- a/app/code/Magento/MediaContentCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentCatalog/etc/di.xml @@ -14,6 +14,22 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentCatalog\Observer\ProductDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="description" xsi:type="string">description</item> + <item name="short_description" xsi:type="string">short_description</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentCatalog\Observer\CategoryDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="image" xsi:type="string">image</item> + <item name="description" xsi:type="string">description</item> + </argument> + </arguments> + </type> <type name="Magento\MediaContentCatalog\Observer\Category"> <arguments> <argument name="fields" xsi:type="array"> diff --git a/app/code/Magento/MediaContentCatalog/etc/events.xml b/app/code/Magento/MediaContentCatalog/etc/events.xml index f68d66eb3cc40..8ec7a30b961ba 100644 --- a/app/code/Magento/MediaContentCatalog/etc/events.xml +++ b/app/code/Magento/MediaContentCatalog/etc/events.xml @@ -9,6 +9,12 @@ <event name="catalog_category_save_after"> <observer name="media_content_catalog_category_save_after" instance="Magento\MediaContentCatalog\Observer\Category" /> </event> + <event name="catalog_product_delete_before"> + <observer name="media_content_catalog_product_delete_before" instance="Magento\MediaContentCatalog\Observer\ProductDelete" /> + </event> + <event name="catalog_category_delete_before"> + <observer name="media_content_catalog_category_delete_before" instance="Magento\MediaContentCatalog\Observer\CategoryDelete" /> + </event> <event name="catalog_product_save_after"> <observer name="media_content_catalog_product_save_after" instance="Magento\MediaContentCatalog\Observer\Product" /> </event> diff --git a/app/code/Magento/MediaContentCms/Observer/BlockDelete.php b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php new file mode 100644 index 0000000000000..582f0a9ec6701 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Observer; + +use Magento\Cms\Model\Block as CmsBlock; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the adminhtml_cmspage_on_delete event and deletes relation between page content and media asset. + */ +class BlockDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'cms_block'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $block = $observer->getEvent()->getData('object'); + $contentAssetLinks = []; + + if ($block instanceof CmsBlock) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $block->getId(), + ] + ); + $assets = $this->extractAssetsFromContent->execute((string) $block->getData($field)); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCms/Observer/PageDelete.php b/app/code/Magento/MediaContentCms/Observer/PageDelete.php new file mode 100644 index 0000000000000..96d2bf89873bd --- /dev/null +++ b/app/code/Magento/MediaContentCms/Observer/PageDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Observer; + +use Magento\Cms\Model\Page as CmsPage; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the cms_page_delete_before event and deletes relation between page content and media asset. + */ +class PageDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'cms_page'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param arry $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $page = $observer->getEvent()->getData('object'); + $contentAssetLinks = []; + + if ($page instanceof CmsPage) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $page->getId(), + ] + ); + + $assets = $this->extractAssetsFromContent->execute((string) $page->getData($field)); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCms/etc/di.xml b/app/code/Magento/MediaContentCms/etc/di.xml index f980936465faf..6f196889540af 100644 --- a/app/code/Magento/MediaContentCms/etc/di.xml +++ b/app/code/Magento/MediaContentCms/etc/di.xml @@ -20,4 +20,18 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentCms\Observer\PageDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentCms\Observer\BlockDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentCms/etc/events.xml b/app/code/Magento/MediaContentCms/etc/events.xml index 7e9abe3bf19c4..94f963f40be15 100644 --- a/app/code/Magento/MediaContentCms/etc/events.xml +++ b/app/code/Magento/MediaContentCms/etc/events.xml @@ -6,8 +6,14 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="cms_page_delete_before"> + <observer name="media_content_cms_page_delete_before" instance="Magento\MediaContentCms\Observer\PageDelete" /> + </event> <event name="cms_page_save_after"> <observer name="media_content_cms_page_save_after" instance="Magento\MediaContentCms\Observer\Page" /> + </event> + <event name="cms_block_delete_before"> + <observer name="media_content_cms_block_delete_before" instance="Magento\MediaContentCms\Observer\BlockDelete" /> </event> <event name="cms_block_save_after"> <observer name="media_content_cms_block_save_after" instance="Magento\MediaContentCms\Observer\Block" /> diff --git a/app/code/Magento/MediaGallery/Model/Asset.php b/app/code/Magento/MediaGallery/Model/Asset.php index 78b9477a70b08..7a4e51709dc0a 100644 --- a/app/code/Magento/MediaGallery/Model/Asset.php +++ b/app/code/Magento/MediaGallery/Model/Asset.php @@ -32,11 +32,21 @@ class Asset implements AssetInterface */ private $title; + /** + * @var string|null + */ + private $description; + /** * @var string|null */ private $source; + /** + * @var string|null + */ + private $hash; + /** * @var string */ @@ -80,7 +90,9 @@ class Asset implements AssetInterface * @param int $size * @param int|null $id * @param string|null $title + * @param string|null $description * @param string|null $source + * @param string|null $hash * @param string|null $createdAt * @param string|null $updatedAt * @param AssetExtensionInterface|null $extensionAttributes @@ -93,7 +105,9 @@ public function __construct( int $size, ?int $id = null, ?string $title = null, + ?string $description = null, ?string $source = null, + ?string $hash = null, ?string $createdAt = null, ?string $updatedAt = null, ?AssetExtensionInterface $extensionAttributes = null @@ -105,7 +119,9 @@ public function __construct( $this->size = $size; $this->id = $id; $this->title = $title; + $this->description = $description; $this->source = $source; + $this->hash = $hash; $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; $this->extensionAttributes = $extensionAttributes; @@ -135,6 +151,14 @@ public function getTitle(): ?string return $this->title; } + /** + * @inheritdoc + */ + public function getDescription(): ?string + { + return $this->description; + } + /** * @inheritdoc */ @@ -143,6 +167,14 @@ public function getSource(): ?string return $this->source; } + /** + * @inheritdoc + */ + public function getHash(): ?string + { + return $this->hash; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php index b2f900233e46a..71e2cb70663f3 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php @@ -94,7 +94,9 @@ public function execute(int $mediaAssetId): AssetInterface 'id' => $mediaAssetData['id'], 'path' => $mediaAssetData['path'], 'title' => $mediaAssetData['title'], + 'description' => $mediaAssetData['description'], 'source' => $mediaAssetData['source'], + 'hash' => $mediaAssetData['hash'], 'contentType' => $mediaAssetData['content_type'], 'width' => $mediaAssetData['width'], 'height' => $mediaAssetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php index d9faad62b2cd1..02512a12f9d07 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php @@ -86,7 +86,9 @@ public function execute(string $path): AssetInterface 'id' => $data['id'], 'path' => $data['path'], 'title' => $data['title'], + 'description' => $data['description'], 'source' => $data['source'], + 'hash' => $data['hash'], 'contentType' => $data['content_type'], 'width' => $data['width'], 'height' => $data['height'], diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php index 4d87c1aa95285..f33c22a18b4b8 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php @@ -10,7 +10,7 @@ use Magento\Cms\Model\Wysiwyg\Images\Storage; use Magento\Framework\Exception\CouldNotSaveException; use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Psr\Log\LoggerInterface; /** @@ -29,23 +29,23 @@ class CreateByPaths implements CreateDirectoriesByPathsInterface private $storage; /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ - private $isPathBlacklisted; + private $isPathExcluded; /** * @param LoggerInterface $logger * @param Storage $storage - * @param IsPathBlacklistedInterface $isPathBlacklisted + * @param IsPathExcludedInterface $isPathExcluded */ public function __construct( LoggerInterface $logger, Storage $storage, - IsPathBlacklistedInterface $isPathBlacklisted + IsPathExcludedInterface $isPathExcluded ) { $this->logger = $logger; $this->storage = $storage; - $this->isPathBlacklisted = $isPathBlacklisted; + $this->isPathExcluded = $isPathExcluded; } /** @@ -55,7 +55,7 @@ public function execute(array $paths): void { $failedPaths = []; foreach ($paths as $path) { - if ($this->isPathBlacklisted->execute($path)) { + if ($this->isPathExcluded->execute($path)) { $failedPaths[] = $path; continue; } diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php index d46fb854fff22..2e45000c07225 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php @@ -10,7 +10,7 @@ use Magento\Cms\Model\Wysiwyg\Images\Storage; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Psr\Log\LoggerInterface; /** @@ -29,23 +29,23 @@ class DeleteByPaths implements DeleteDirectoriesByPathsInterface private $storage; /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ - private $isPathBlacklisted; + private $isPathExcluded; /** * @param LoggerInterface $logger * @param Storage $storage - * @param IsPathBlacklistedInterface $isPathBlacklisted + * @param IsPathExcludedInterface $isPathExcluded */ public function __construct( LoggerInterface $logger, Storage $storage, - IsPathBlacklistedInterface $isPathBlacklisted + IsPathExcludedInterface $isPathExcluded ) { $this->logger = $logger; $this->storage = $storage; - $this->isPathBlacklisted = $isPathBlacklisted; + $this->isPathExcluded = $isPathExcluded; } /** @@ -55,7 +55,7 @@ public function execute(array $paths): void { $failedPaths = []; foreach ($paths as $path) { - if ($this->isPathBlacklisted->execute($path)) { + if ($this->isPathExcluded->execute($path)) { $failedPaths[] = $path; continue; } diff --git a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php index 91f16d246f636..3d9911c805efb 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php @@ -15,9 +15,9 @@ class Converter implements ConverterInterface { /** - * Blacklist tag name + * Excluded list tag name */ - private const BLACKLIST_TAG_NAME = 'blacklist'; + private const EXCLUDED_LIST_TAG_NAME = 'exclude'; /** * Patterns tag name @@ -43,12 +43,12 @@ public function convert($source): array throw new \InvalidArgumentException('The source should be instance of DOMDocument'); } - foreach ($source->getElementsByTagName(self::BLACKLIST_TAG_NAME) as $blacklist) { - $result[self::BLACKLIST_TAG_NAME] = []; - foreach ($blacklist->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) { - $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME] = []; + foreach ($source->getElementsByTagName(self::EXCLUDED_LIST_TAG_NAME) as $excludedList) { + $result[self::EXCLUDED_LIST_TAG_NAME] = []; + foreach ($excludedList->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) { + $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME] = []; foreach ($patterns->getElementsByTagName(self::PATTERN_TAG_NAME) as $pattern) { - $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME] + $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME] [$pattern->attributes->getNamedItem('name')->nodeValue] = $pattern->nodeValue; } } diff --git a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php similarity index 68% rename from app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php rename to app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php index 8fdd4f70d5060..29ed5fbf04ecd 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php +++ b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php @@ -8,14 +8,14 @@ namespace Magento\MediaGallery\Model\Directory; use Magento\Framework\Config\DataInterface; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; /** * Media gallery directory config */ -class BlacklistPatternsConfig implements BlacklistPatternsConfigInterface +class ExcludedPatternsConfig implements ExcludedPatternsConfigInterface { - private const XML_PATH_BLACKLIST_PATTERNS = 'blacklist/patterns'; + private const XML_PATH_EXCLUDED_PATTERNS = 'exclude/patterns'; /** * @var DataInterface @@ -37,6 +37,6 @@ public function __construct(DataInterface $data) */ public function get() : array { - return $this->data->get(self::XML_PATH_BLACKLIST_PATTERNS); + return $this->data->get(self::XML_PATH_EXCLUDED_PATTERNS); } } diff --git a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php similarity index 61% rename from app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php rename to app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php index 0191b357aaefa..8fb0e03b76548 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php +++ b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php @@ -7,23 +7,23 @@ namespace Magento\MediaGallery\Model\Directory; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; /** - * Check if the path is blacklisted for media gallery. Directory path may be blacklisted if it's reserved by the system + * Check if the path is excluded for media gallery. Directory path may be blacklisted if it's reserved by the system */ -class IsBlacklisted implements IsPathBlacklistedInterface +class IsExcluded implements IsPathExcludedInterface { /** - * @var BlacklistPatternsConfigInterface + * @var ExcludedPatternsConfigInterface */ private $config; /** - * @param BlacklistPatternsConfigInterface $config + * @param ExcludedPatternsConfigInterface $config */ - public function __construct(BlacklistPatternsConfigInterface $config) + public function __construct(ExcludedPatternsConfigInterface $config) { $this->config = $config; } diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php index 53185939b2283..f73162b775683 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php @@ -65,7 +65,9 @@ public function execute(array $ids): array 'id' => $assetData['id'], 'path' => $assetData['path'], 'title' => $assetData['title'], + 'description' => $assetData['description'], 'source' => $assetData['source'], + 'hash' => $assetData['hash'], 'contentType' => $assetData['content_type'], 'width' => $assetData['width'], 'height' => $assetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php index 5593083d9673a..b25d2e22aabd4 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php @@ -66,7 +66,9 @@ public function execute(array $paths): array 'id' => $assetData['id'], 'path' => $assetData['path'], 'title' => $assetData['title'], + 'description' => $assetData['description'], 'source' => $assetData['source'], + 'hash' => $assetData['hash'], 'contentType' => $assetData['content_type'], 'width' => $assetData['width'], 'height' => $assetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php index ec08addf93462..801279aa7fd7d 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php @@ -60,7 +60,9 @@ public function execute(array $assets): void 'id' => $asset->getId(), 'path' => $asset->getPath(), 'title' => $asset->getTitle(), + 'description' => $asset->getDescription(), 'source' => $asset->getSource(), + 'hash' => $asset->getHash(), 'content_type' => $asset->getContentType(), 'width' => $asset->getWidth(), 'height' => $asset->getHeight(), diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php index 09ce7ffe8ff20..5f99163db8f12 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php @@ -28,7 +28,9 @@ class GetByIdExceptionDuringMediaAssetInitializationTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php index 89efae07360b4..3b47b0036224b 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php @@ -29,7 +29,9 @@ class GetByIdExceptionOnGetDataTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php index 8b805d0256e37..2c24899746473 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php @@ -29,7 +29,9 @@ class GetByIdSuccessfulTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php similarity index 70% rename from app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php rename to app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php index c96fd2ee54512..cc57b043954d7 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php @@ -8,45 +8,45 @@ namespace Magento\MediaGallery\Test\Unit\Model\Directory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\MediaGallery\Model\Directory\IsBlacklisted; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGallery\Model\Directory\IsExcluded; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test for IsBlacklisted + * Test for IsExcluded */ -class IsBlacklistedTest extends TestCase +class IsExcludedTest extends TestCase { /** - * @var IsBlacklisted + * @var IsExcluded */ private $object; /** - * @var BlacklistPatternsConfigInterface|MockObject + * @var ExcludedPatternsConfigInterface|MockObject */ - private $config; + private $configMock; /** * Initialize basic test class mocks */ protected function setUp(): void { - $this->config = $this->getMockBuilder(BlacklistPatternsConfigInterface::class) + $this->configMock = $this->getMockBuilder(ExcludedPatternsConfigInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->config->expects($this->at(0))->method('get')->willReturn([ + $this->configMock->expects($this->at(0))->method('get')->willReturn([ 'tmp' => '/pub\/media\/tmp/', 'captcha' => '/pub\/media\/captcha/' ]); - $this->object = (new ObjectManager($this))->getObject(IsBlacklisted::class, [ - 'config' => $this->config + $this->object = (new ObjectManager($this))->getObject(IsExcluded::class, [ + 'config' => $this->configMock ]); } /** - * Test if the directory path is blacklisted + * Test if the directory path is excluded * * @param string $path * @param bool $isExcluded diff --git a/app/code/Magento/MediaGallery/etc/db_schema.xml b/app/code/Magento/MediaGallery/etc/db_schema.xml index 31a764ef00c4d..1001737daa8a7 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema.xml +++ b/app/code/Magento/MediaGallery/etc/db_schema.xml @@ -10,7 +10,9 @@ <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> <column xsi:type="varchar" name="path" length="255" nullable="true" comment="Path"/> <column xsi:type="varchar" name="title" length="255" nullable="true" comment="Title"/> + <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="source" length="255" nullable="true" comment="Source"/> + <column xsi:type="varchar" name="hash" length="255" nullable="true" comment="File hash"/> <column xsi:type="varchar" name="content_type" length="255" nullable="true" comment="Content Type"/> <column xsi:type="int" name="width" unsigned="true" nullable="false" identity="false" default="0" comment="Width"/> <column xsi:type="int" name="height" unsigned="true" nullable="false" identity="false" default="0" comment="Height"/> diff --git a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json index 8f5098caa9753..b32dfbf082175 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json +++ b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json @@ -4,7 +4,9 @@ "id": true, "path": true, "title": true, + "description": true, "source": true, + "hash": true, "content_type": true, "width": true, "height": true, diff --git a/app/code/Magento/MediaGallery/etc/di.xml b/app/code/Magento/MediaGallery/etc/di.xml index a85c26e275226..bedb78758786b 100644 --- a/app/code/Magento/MediaGallery/etc/di.xml +++ b/app/code/Magento/MediaGallery/etc/di.xml @@ -21,7 +21,7 @@ <preference for="Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface" type="Magento\MediaGallery\Model\Directory\Command\CreateByPaths"/> <preference for="Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface" type="Magento\MediaGallery\Model\Directory\Command\DeleteByPaths"/> - <preference for="Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface" type="Magento\MediaGallery\Model\Directory\IsBlacklisted"/> + <preference for="Magento\MediaGalleryApi\Api\IsPathExcludedInterface" type="Magento\MediaGallery\Model\Directory\IsExcluded"/> <preference for="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface" type="Magento\MediaGallery\Model\ResourceModel\DeleteAssetsByPaths"/> <preference for="Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface" type="Magento\MediaGallery\Model\ResourceModel\GetAssetsByIds"/> @@ -40,7 +40,7 @@ <argument name="converter" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\Converter</argument> <argument name="schemaLocator" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\SchemaLocator</argument> <argument name="idAttributes" xsi:type="array"> - <item name="/config/blacklist/patterns/pattern" xsi:type="string">name</item> + <item name="/config/exclude/patterns/pattern" xsi:type="string">name</item> </argument> </arguments> </virtualType> @@ -50,11 +50,10 @@ <argument name="cacheId" xsi:type="string">Media_Gallery_Patterns_CacheId</argument> </arguments> </virtualType> - <type name="Magento\MediaGallery\Model\Directory\BlacklistPatternsConfig"> + <type name="Magento\MediaGallery\Model\Directory\ExcludedPatternsConfig"> <arguments> <argument name="data" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\Data</argument> </arguments> </type> - - <preference for="Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface" type="Magento\MediaGallery\Model\Directory\BlacklistPatternsConfig"/> + <preference for="Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface" type="Magento\MediaGallery\Model\Directory\ExcludedPatternsConfig"/> </config> diff --git a/app/code/Magento/MediaGallery/etc/directory.xml b/app/code/Magento/MediaGallery/etc/directory.xml index 92f50b2dd0a30..42094aff72640 100644 --- a/app/code/Magento/MediaGallery/etc/directory.xml +++ b/app/code/Magento/MediaGallery/etc/directory.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_MediaGalleryApi:etc/directory.xsd"> - <blacklist> + <exclude> <patterns> <pattern name="captcha">/^captcha/</pattern> <pattern name="customer">/^customer/</pattern> @@ -17,5 +17,5 @@ <pattern name="tmp">/^tmp/</pattern> <pattern name="directories-with-dots">/^\./</pattern> </patterns> - </blacklist> + </exclude> </config> diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php index 5df420a274933..a747cb963baab 100644 --- a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php @@ -38,6 +38,13 @@ public function getPath(): string; */ public function getTitle(): ?string; + /** + * Get description + * + * @return string|null + */ + public function getDescription(): ?string; + /** * Get the name of the channel/stock/integration file was retrieved from. null if not identified. * @@ -45,6 +52,13 @@ public function getTitle(): ?string; */ public function getSource(): ?string; + /** + * Get file hash + * + * @return string|null + */ + public function getHash(): ?string; + /** * Get content type * diff --git a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php similarity index 71% rename from app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php rename to app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php index cbd23ec3fbde7..1e41debb1b1c5 100644 --- a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php @@ -8,12 +8,12 @@ namespace Magento\MediaGalleryApi\Api; /** - * Check if the path is blacklisted for media gallery. + * Check if the path is excluded for media gallery. * - * Directory path may be blacklisted if it's reserved by the system. + * Directory path may be excluded if it's reserved by the system. * @api */ -interface IsPathBlacklistedInterface +interface IsPathExcludedInterface { /** * Check if the path is excluded from displaying and processing in the media gallery diff --git a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php similarity index 75% rename from app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php rename to app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php index b4710f32e0c46..dd82f87780a49 100644 --- a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php @@ -7,9 +7,9 @@ namespace Magento\MediaGalleryApi\Model; /** - * Returns list of blacklist regexp patterns + * Returns list of excluded regexp patterns */ -interface BlacklistPatternsConfigInterface +interface ExcludedPatternsConfigInterface { /** * Get regexp patterns diff --git a/app/code/Magento/MediaGalleryApi/etc/directory.xsd b/app/code/Magento/MediaGalleryApi/etc/directory.xsd index 2ad76c8fcc9f2..2fb4fed028469 100644 --- a/app/code/Magento/MediaGalleryApi/etc/directory.xsd +++ b/app/code/Magento/MediaGalleryApi/etc/directory.xsd @@ -11,14 +11,14 @@ <xs:complexType name="configType"> <xs:sequence> - <xs:element type="blacklistType" name="blacklist" maxOccurs="unbounded" minOccurs="1"/> + <xs:element type="excludeType" name="exclude" maxOccurs="unbounded" minOccurs="1"/> </xs:sequence> </xs:complexType> - <xs:complexType name="blacklistType"> + <xs:complexType name="excludeType"> <xs:annotation> <xs:documentation> - Blacklist used for excluding directories from media gallery rendering and operations + List used for excluding directories from media gallery rendering and operations </xs:documentation> </xs:annotation> <xs:sequence> diff --git a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml index eaced3f642f70..f1ec76a877368 100644 --- a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml +++ b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml @@ -6,9 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_MediaGalleryApi:etc/directory.xsd"> - <blacklist> + <exclude> <patterns> <pattern name="catalog">/^catalog\/product/</pattern> </patterns> - </blacklist> + </exclude> </config> diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml index 429482e5795bf..768c97ef316f7 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml @@ -40,3 +40,12 @@ </form> </div> </div> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "newsletter-validate-detail" + } + } + } +</script> diff --git a/app/code/Magento/Payment/Block/Transparent/Redirect.php b/app/code/Magento/Payment/Block/Transparent/Redirect.php index 1be6dec4cc1d8..97a09df38d120 100644 --- a/app/code/Magento/Payment/Block/Transparent/Redirect.php +++ b/app/code/Magento/Payment/Block/Transparent/Redirect.php @@ -53,10 +53,21 @@ public function getRedirectUrl(): string /** * Returns params to be redirected. * + * Encodes invalid UTF-8 values to UTF-8 to prevent character escape error. + * Some payment methods like PayPal, send data in merchant defined language encoding + * which can be different from the system character encoding (UTF-8). + * * @return array */ public function getPostParams(): array { - return (array)$this->_request->getPostValue(); + $params = []; + foreach ($this->_request->getPostValue() as $name => $value) { + if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) { + $value = utf8_encode($value); + } + $params[$name] = $value; + } + return $params; } } diff --git a/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php new file mode 100644 index 0000000000000..1cd1230a14634 --- /dev/null +++ b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Test\Unit\Block\Transparent; + +use Magento\Payment\Block\Transparent\Redirect; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class RedirectTest extends TestCase +{ + /** + * @var \Magento\Framework\View\Element\Context|MockObject + */ + private $context; + /** + * @var \Magento\Framework\UrlInterface|MockObject + */ + private $url; + /** + * @var Redirect + */ + private $model; + /** + * @var \Magento\Framework\App\RequestInterface|MockObject + */ + private $request; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->context->method('getRequest') + ->willReturn($this->request); + $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); + $this->model = new Redirect( + $this->context, + $this->url + ); + } + + /** + * @param array $postData + * @param array $expected + * @dataProvider getPostParamsDataProvider + */ + public function testGetPostParams(array $postData, array $expected): void + { + $this->request->method('getPostValue') + ->willReturn($postData); + $this->assertEquals($expected, $this->model->getPostParams()); + } + + /** + * @return array + */ + public function getPostParamsDataProvider(): array + { + return [ + [ + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ], + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ] + ], + [ + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => mb_convert_encoding('Ãtienne', 'ISO-8859-1'), + 'BILLTOFIRSTNAME' => mb_convert_encoding('Ãillin', 'ISO-8859-1'), + ], + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ] + ] + ]; + } +} diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 39148f990b714..4366ef7aaf969 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -1019,6 +1019,13 @@ public function collectShippingRates() */ public function requestShippingRates(AbstractItem $item = null) { + $storeId = $this->getQuote()->getStoreId() ?: $this->storeManager->getStore()->getId(); + $taxInclude = $this->_scopeConfig->getValue( + 'tax/calculation/price_includes_tax', + ScopeInterface::SCOPE_STORE, + $storeId + ); + /** @var $request RateRequest */ $request = $this->_rateRequestFactory->create(); $request->setAllItems($item ? [$item] : $this->getAllItems()); @@ -1028,9 +1035,11 @@ public function requestShippingRates(AbstractItem $item = null) $request->setDestStreet($this->getStreetFull()); $request->setDestCity($this->getCity()); $request->setDestPostcode($this->getPostcode()); - $request->setPackageValue($item ? $item->getBaseRowTotal() : $this->getBaseSubtotal()); + $baseSubtotal = $taxInclude ? $this->getBaseSubtotalTotalInclTax() : $this->getBaseSubtotal(); + $request->setPackageValue($item ? $item->getBaseRowTotal() : $baseSubtotal); + $baseSubtotalWithDiscount = $baseSubtotal + $this->getBaseDiscountAmount(); $packageWithDiscount = $item ? $item->getBaseRowTotal() - - $item->getBaseDiscountAmount() : $this->getBaseSubtotalWithDiscount(); + $item->getBaseDiscountAmount() : $baseSubtotalWithDiscount; $request->setPackageValueWithDiscount($packageWithDiscount); $request->setPackageWeight($item ? $item->getRowWeight() : $this->getWeight()); $request->setPackageQty($item ? $item->getQty() : $this->getItemQty()); @@ -1038,8 +1047,7 @@ public function requestShippingRates(AbstractItem $item = null) /** * Need for shipping methods that use insurance based on price of physical products */ - $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $this->getBaseSubtotal() - - $this->getBaseVirtualAmount(); + $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $baseSubtotal - $this->getBaseVirtualAmount(); $request->setPackagePhysicalValue($packagePhysicalValue); $request->setFreeMethodWeight($item ? 0 : $this->getFreeMethodWeight()); @@ -1047,12 +1055,10 @@ public function requestShippingRates(AbstractItem $item = null) /** * Store and website identifiers specified from StoreManager */ + $request->setStoreId($storeId); if ($this->getQuote()->getStoreId()) { - $storeId = $this->getQuote()->getStoreId(); - $request->setStoreId($storeId); $request->setWebsiteId($this->storeManager->getStore($storeId)->getWebsiteId()); } else { - $request->setStoreId($this->storeManager->getStore()->getId()); $request->setWebsiteId($this->storeManager->getWebsite()->getId()); } $request->setFreeShipping($this->getFreeShipping()); diff --git a/app/code/Magento/Quote/Model/Quote/Item/Processor.php b/app/code/Magento/Quote/Model/Quote/Item/Processor.php index ef4b853862681..c6bef1cc80bfb 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Processor.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Processor.php @@ -97,7 +97,9 @@ public function prepare(Item $item, DataObject $request, Product $candidate): vo $item->addQty($candidate->getCartQty()); $customPrice = $request->getCustomPrice(); - $item->setPrice($candidate->getFinalPrice()); + if (!$item->getParentItem() || $item->getParentItem()->isChildrenCalculated()) { + $item->setPrice($candidate->getFinalPrice()); + } if (!empty($customPrice)) { $item->setCustomPrice($customPrice); $item->setOriginalCustomPrice($customPrice); diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php index 48945dacd1738..749e9944a6ad3 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php @@ -230,7 +230,8 @@ public function subtractProductFromQuotes($product) 'items_qty' => new \Zend_Db_Expr( $connection->quoteIdentifier('q.items_qty') . ' - ' . $connection->quoteIdentifier('qi.qty') ), - 'items_count' => new \Zend_Db_Expr($ifSql) + 'items_count' => new \Zend_Db_Expr($ifSql), + 'updated_at' => 'q.updated_at', ] )->join( ['qi' => $this->getTable('quote_item')], @@ -277,21 +278,27 @@ public function markQuotesRecollect($productIds) { $tableQuote = $this->getTable('quote'); $tableItem = $this->getTable('quote_item'); - $subSelect = $this->getConnection()->select()->from( - $tableItem, - ['entity_id' => 'quote_id'] - )->where( - 'product_id IN ( ? )', - $productIds - )->group( - 'quote_id' - ); - - $select = $this->getConnection()->select()->join( - ['t2' => $subSelect], - 't1.entity_id = t2.entity_id', - ['trigger_recollect' => new \Zend_Db_Expr('1')] - ); + $subSelect = $this->getConnection() + ->select() + ->from( + $tableItem, + ['entity_id' => 'quote_id'] + )->where( + 'product_id IN ( ? )', + $productIds + )->group( + 'quote_id' + ); + $select = $this->getConnection() + ->select() + ->join( + ['t2' => $subSelect], + 't1.entity_id = t2.entity_id', + [ + 'trigger_recollect' => new \Zend_Db_Expr('1'), + 'updated_at' => 't1.updated_at', + ] + ); $updateQuery = $select->crossUpdateFromSelect(['t1' => $tableQuote]); $this->getConnection()->query($updateQuery); diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml new file mode 100755 index 0000000000000..a14be3b533fa8 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml @@ -0,0 +1,27 @@ +<?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="CustomerCart" type="CustomerCart"> + <var key="customer_id" entityType="customer" entityKey="id"/> + </entity> + + <entity name="CustomerAddressInformation" type="CustomerAddressInformation"> + <var key="cart_id" entityKey="return" entityType="CustomerCart"/> + <requiredEntity type="shipping_address">ShippingAddressTX</requiredEntity> + <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> + <data key="shipping_method_code">flatrate</data> + <data key="shipping_carrier_code">flatrate</data> + </entity> + + <entity name="CustomerOrderPaymentMethod" type="CustomerPaymentInformation"> + <var key="cart_id" entityKey="return" entityType="CustomerCart"/> + <requiredEntity type="payment_method">PaymentMethodCheckMoneyOrder</requiredEntity> + <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml new file mode 100644 index 0000000000000..3681245311188 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml @@ -0,0 +1,16 @@ +<?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="CustomerCartItem" type="CustomerCartItem"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + <var key="sku" entityKey="sku" entityType="product"/> + <data key="qty">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml new file mode 100644 index 0000000000000..f5555394f8d4d --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml @@ -0,0 +1,20 @@ +<?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="CreateCustomerCartItem" dataType="CustomerCartItem" type="create" auth="adminOauth" url="/V1/carts/mine/items" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CustomerCartItem"> + <field key="quote_id" type="string">string</field> + <field key="sku" type="string">string</field> + <field key="qty">integer</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml new file mode 100644 index 0000000000000..f233954f2cdcf --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml @@ -0,0 +1,63 @@ +<?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="CreateCustomerCart" dataType="CustomerCart" type="create" + auth="adminOauth" url="/V1/carts/mine" method="POST" > + <contentType>application/json</contentType> + <field key="customer_id">string</field> + </operation> + + <operation name="AddAddressInfoToCustomerCart" dataType="CustomerAddressInformation" type="create" auth="adminOauth" url="/V1/carts/mine/shipping-information" method="POST"> + <contentType>application/json</contentType> + <field key="cart_id">string</field> + <object key="addressInformation" dataType="CustomerAddressInformation"> + <object key="shipping_address" dataType="shipping_address"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </object> + <object key="billing_address" dataType="billing_address"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </object> + <field key="shipping_method_code">string</field> + <field key="shipping_carrier_code">string</field> + </object> + </operation> + + <operation name="SendCustomerPaymentInformation" dataType="CustomerPaymentInformation" type="update" auth="adminOauth" url="/V1/carts/mine/payment-information" method="POST"> + <contentType>application/json</contentType> + <field key="cart_id">string</field> + <object key="paymentMethod" dataType="payment_method"> + <field key="method">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php index a8fd794c08757..d4f6778a2ccb8 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php @@ -352,10 +352,40 @@ public function testRequestShippingRates() $currentCurrencyCode = 'UAH'; + $this->quote->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + + $this->storeManager->expects($this->at(0)) + ->method('getStore') + ->with($storeId) + ->willReturn($this->store); + $this->store->expects($this->any()) + ->method('getWebsiteId') + ->willReturn($webSiteId); + + $this->scopeConfig->expects($this->exactly(1)) + ->method('getValue') + ->with( + 'tax/calculation/price_includes_tax', + ScopeInterface::SCOPE_STORE, + $storeId + ) + ->willReturn(1); + /** @var RateRequest */ $request = $this->getMockBuilder(RateRequest::class) ->disableOriginalConstructor() - ->setMethods(['setStoreId', 'setWebsiteId', 'setBaseCurrency', 'setPackageCurrency']) + ->setMethods( + [ + 'setStoreId', + 'setWebsiteId', + 'setBaseCurrency', + 'setPackageCurrency', + 'getBaseSubtotalTotalInclTax', + 'getBaseSubtotal' + ] + ) ->getMock(); /** @var Collection */ @@ -434,13 +464,6 @@ public function testRequestShippingRates() $this->storeManager->method('getStore') ->willReturn($this->store); - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->willReturn($this->website); - - $this->store->method('getId') - ->willReturn($storeId); - $this->store->method('getBaseCurrency') ->willReturn($baseCurrency); @@ -452,10 +475,6 @@ public function testRequestShippingRates() ->method('getCurrentCurrencyCode') ->willReturn($currentCurrencyCode); - $this->website->expects($this->once()) - ->method('getId') - ->willReturn($webSiteId); - $this->addressRateFactory->expects($this->once()) ->method('create') ->willReturn($rate); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php index cbcb7dd0adc3c..3025a72410671 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php @@ -77,7 +77,16 @@ protected function setUp(): void $this->itemMock = $this->getMockBuilder(Item::class) ->addMethods(['setOriginalCustomPrice']) - ->onlyMethods(['getId', 'setOptions', 'setProduct', 'addQty', 'setCustomPrice', 'setData', 'setPrice']) + ->onlyMethods([ + 'getId', + 'setOptions', + 'setProduct', + 'addQty', + 'setCustomPrice', + 'setData', + 'setPrice', + 'getParentItem' + ]) ->disableOriginalConstructor() ->getMock(); $this->quoteItemFactoryMock->expects($this->any()) @@ -438,4 +447,41 @@ public function testPrepareWithResetCountAndNotStickAndSameItemId() $this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock); } + + /** + * @param bool $isChildrenCalculated + * @dataProvider prepareChildProductDataProvider + */ + public function testPrepareChildProduct(bool $isChildrenCalculated): void + { + $finalPrice = 10; + $this->objectMock->method('getResetCount') + ->willReturn(false); + $this->productMock->method('getFinalPrice') + ->willReturn($finalPrice); + $this->itemMock->expects($isChildrenCalculated ? $this->once() : $this->never()) + ->method('setPrice') + ->with($finalPrice) + ->willReturnSelf(); + $parentItem = $this->createConfiguredMock( + \Magento\Quote\Model\Quote\Item::class, + [ + 'isChildrenCalculated' => $isChildrenCalculated + ] + ); + $this->itemMock->method('getParentItem') + ->willReturn($parentItem); + $this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock); + } + + /** + * @return array + */ + public function prepareChildProductDataProvider(): array + { + return [ + [false], + [true] + ]; + } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index f73daa715c1df..e959c19a7cbe4 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -51,7 +51,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s $shippingAddressInput = current($shippingAddressesInput) ?? []; $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; - if (!$customerAddressId && !isset($shippingAddressInput['address']['save_in_address_book'])) { + if (!$customerAddressId + && isset($shippingAddressInput['address']) + && !isset($shippingAddressInput['address']['save_in_address_book']) + ) { $shippingAddressInput['address']['save_in_address_book'] = true; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php index b2526bdc04e98..654a4bb558632 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php @@ -42,12 +42,12 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s } $shippingMethodInput = current($shippingMethodsInput); - if (!isset($shippingMethodInput['carrier_code']) || empty($shippingMethodInput['carrier_code'])) { + if (empty($shippingMethodInput['carrier_code'])) { throw new GraphQlInputException(__('Required parameter "carrier_code" is missing.')); } $carrierCode = $shippingMethodInput['carrier_code']; - if (!isset($shippingMethodInput['method_code']) || empty($shippingMethodInput['method_code'])) { + if (empty($shippingMethodInput['method_code'])) { throw new GraphQlInputException(__('Required parameter "method_code" is missing.')); } $methodCode = $shippingMethodInput['method_code']; diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php new file mode 100644 index 0000000000000..c2e94b215956e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GiftMessage\Api\Data\MessageInterface; +use Magento\GiftMessage\Api\Data\MessageInterfaceFactory; +use Magento\GiftMessage\Api\ItemRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; + +/** + * Class contain update cart items methods + */ +class UpdateCartItems +{ + /** + * @var CartItemRepositoryInterface + */ + private $cartItemRepository; + + /** + * @var UpdateCartItem + */ + private $updateCartItem; + + /** + * @var ItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @var MessageInterfaceFactory + */ + private $giftMessageFactory; + + /** + * @param CartItemRepositoryInterface $cartItemRepository + * @param UpdateCartItem $updateCartItem + * @param ItemRepositoryInterface $itemRepository + * @param GiftMessageHelper $giftMessageHelper + * @param MessageInterfaceFactory $giftMessageFactory + */ + public function __construct( + CartItemRepositoryInterface $cartItemRepository, + UpdateCartItem $updateCartItem, + ItemRepositoryInterface $itemRepository, + GiftMessageHelper $giftMessageHelper, + MessageInterfaceFactory $giftMessageFactory + ) { + $this->cartItemRepository = $cartItemRepository; + $this->updateCartItem = $updateCartItem; + $this->itemRepository = $itemRepository; + $this->giftMessageHelper = $giftMessageHelper; + $this->giftMessageFactory = $giftMessageFactory; + } + + /** + * Process cart items + * + * @param Quote $cart + * @param array $items + * + * @throws GraphQlInputException + * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function processCartItems(Quote $cart, array $items): void + { + foreach ($items as $item) { + if (empty($item['cart_item_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); + } + + $itemId = (int)$item['cart_item_id']; + $customizableOptions = $item['customizable_options'] ?? []; + $cartItem = $cart->getItemById($itemId); + + if ($cartItem && $cartItem->getParentItemId()) { + throw new GraphQlInputException(__('Child items may not be updated.')); + } + + if (count($customizableOptions) === 0 && !isset($item['quantity'])) { + throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); + } + + $quantity = (float)$item['quantity']; + + if ($quantity <= 0.0) { + $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + } else { + $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); + } + + if (!empty($item['gift_message'])) { + try { + if (!$this->giftMessageHelper->isMessagesAllowed('items', $cartItem)) { + continue; + } + if (!$this->giftMessageHelper->isMessagesAllowed('item', $cartItem)) { + continue; + } + + /** @var MessageInterface $giftItemMessage */ + $giftItemMessage = $this->itemRepository->get($cart->getEntityId(), $itemId); + + if (empty($giftItemMessage)) { + /** @var MessageInterface $giftMessage */ + $giftMessage = $this->giftMessageFactory->create(); + $this->updateGiftMessageForItem($cart, $giftMessage, $item, $itemId); + continue; + } + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__('Gift Message cannot be updated.')); + } + + $this->updateGiftMessageForItem($cart, $giftItemMessage, $item, $itemId); + } + } + } + + /** + * Update Gift Message for Quote item + * + * @param Quote $cart + * @param MessageInterface $giftItemMessage + * @param array $item + * @param int $itemId + * + * @throws GraphQlInputException + */ + private function updateGiftMessageForItem(Quote $cart, MessageInterface $giftItemMessage, array $item, int $itemId) + { + try { + $giftItemMessage->setRecipient($item['gift_message']['to']); + $giftItemMessage->setSender($item['gift_message']['from']); + $giftItemMessage->setMessage($item['gift_message']['message']); + $this->itemRepository->save($cart->getEntityId(), $giftItemMessage, $itemId); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__('Gift Message cannot be updated')); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php index 0be95eccc39e5..e8aa8d612c670 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php @@ -7,17 +7,12 @@ namespace Magento\QuoteGraphQl\Model\Resolver; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; -use Magento\Quote\Api\CartManagementInterface; -use Magento\Quote\Model\QuoteIdMaskFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; +use Magento\Quote\Model\Cart\CustomerCartResolver; /** * Get cart for the customer @@ -25,48 +20,19 @@ class CustomerCart implements ResolverInterface { /** - * @var CreateEmptyCartForCustomer + * @var CustomerCartResolver */ - private $createEmptyCartForCustomer; + private $customerCartResolver; /** - * @var CartManagementInterface - */ - private $cartManagement; - - /** - * @var QuoteIdMaskFactory - */ - private $quoteIdMaskFactory; - - /** - * @var QuoteIdMaskResourceModel - */ - private $quoteIdMaskResourceModel; - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedQuoteId; - - /** - * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer - * @param CartManagementInterface $cartManagement - * @param QuoteIdMaskFactory $quoteIdMaskFactory - * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel - * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + * CustomerCart constructor. + * + * @param CustomerCartResolver $customerCartResolver */ public function __construct( - CreateEmptyCartForCustomer $createEmptyCartForCustomer, - CartManagementInterface $cartManagement, - QuoteIdMaskFactory $quoteIdMaskFactory, - QuoteIdMaskResourceModel $quoteIdMaskResourceModel, - QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + CustomerCartResolver $customerCartResolver ) { - $this->createEmptyCartForCustomer = $createEmptyCartForCustomer; - $this->cartManagement = $cartManagement; - $this->quoteIdMaskFactory = $quoteIdMaskFactory; - $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; - $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + $this->customerCartResolver = $customerCartResolver; } /** @@ -76,22 +42,17 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value { $currentUserId = $context->getUserId(); - /** @var ContextInterface $context */ + /** + * @var ContextInterface $context + */ if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The request is allowed for logged in customer')); } - try { - $cart = $this->cartManagement->getCartForCustomer($currentUserId); - } catch (NoSuchEntityException $e) { - $this->createEmptyCartForCustomer->execute($currentUserId, null); - $cart = $this->cartManagement->getCartForCustomer($currentUserId); - } - $maskedId = $this->quoteIdToMaskedQuoteId->execute((int) $cart->getId()); - if (empty($maskedId)) { - $quoteIdMask = $this->quoteIdMaskFactory->create(); - $quoteIdMask->setQuoteId((int) $cart->getId()); - $this->quoteIdMaskResourceModel->save($quoteIdMask); + try { + $cart = $this->customerCartResolver->resolve($currentUserId); + } catch (\Exception $e) { + $cart = null; } return [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php index dd4ce8fe7f7a6..c2e4bfa44c9bb 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php @@ -71,14 +71,15 @@ 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'])) { + if (empty($args['input']['cart_id'])) { 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'])) { + if (empty($args['input']['payment_method']['code'])) { throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.')); } + + $maskedCartId = $args['input']['cart_id']; $paymentData = $args['input']['payment_method']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index fa90f08e4b553..005baaad0e1e5 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -14,53 +14,43 @@ use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Quote\Api\CartItemRepositoryInterface; use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; -use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider; /** * @inheritdoc */ class UpdateCartItems implements ResolverInterface { - /** - * @var UpdateCartItem - */ - private $updateCartItem; - /** * @var GetCartForUser */ private $getCartForUser; /** - * @var CartItemRepositoryInterface + * @var CartRepositoryInterface */ - private $cartItemRepository; + private $cartRepository; /** - * @var CartRepositoryInterface + * @var UpdateCartItemsProvider */ - private $cartRepository; + private $updateCartItems; /** - * @param GetCartForUser $getCartForUser - * @param CartItemRepositoryInterface $cartItemRepository - * @param UpdateCartItem $updateCartItem + * @param GetCartForUser $getCartForUser * @param CartRepositoryInterface $cartRepository + * @param UpdateCartItemsProvider $updateCartItems */ public function __construct( GetCartForUser $getCartForUser, - CartItemRepositoryInterface $cartItemRepository, - UpdateCartItem $updateCartItem, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + UpdateCartItemsProvider $updateCartItems ) { $this->getCartForUser = $getCartForUser; - $this->cartItemRepository = $cartItemRepository; - $this->updateCartItem = $updateCartItem; $this->cartRepository = $cartRepository; + $this->updateCartItems = $updateCartItems; } /** @@ -71,6 +61,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } + $maskedCartId = $args['input']['cart_id']; if (empty($args['input']['cart_items']) @@ -78,13 +69,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ) { throw new GraphQlInputException(__('Required parameter "cart_items" is missing.')); } - $cartItems = $args['input']['cart_items']; + $cartItems = $args['input']['cart_items']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); try { - $this->processCartItems($cart, $cartItems); + $this->updateCartItems->processCartItems($cart, $cartItems); $this->cartRepository->save($cart); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); @@ -98,39 +89,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ], ]; } - - /** - * Process cart items - * - * @param Quote $cart - * @param array $items - * @throws GraphQlInputException - * @throws LocalizedException - */ - private function processCartItems(Quote $cart, array $items): void - { - foreach ($items as $item) { - if (empty($item['cart_item_id'])) { - throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); - } - $itemId = (int)$item['cart_item_id']; - $customizableOptions = $item['customizable_options'] ?? []; - - $cartItem = $cart->getItemById($itemId); - if ($cartItem && $cartItem->getParentItemId()) { - throw new GraphQlInputException(__('Child items may not be updated.')); - } - - if (count($customizableOptions) === 0 && !isset($item['quantity'])) { - throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); - } - $quantity = (float)$item['quantity']; - - if ($quantity <= 0.0) { - $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); - } else { - $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); - } - } - } } diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 0652d39b5f426..25f089cf75a62 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -13,7 +13,8 @@ "magento/module-customer-graph-ql": "*", "magento/module-sales": "*", "magento/module-directory": "*", - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "*", + "magento/module-gift-message": "*" }, "suggest": { "magento/module-graph-ql-cache": "*" diff --git a/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php new file mode 100644 index 0000000000000..a8a9f78df7f28 --- /dev/null +++ b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Rule\Test\Mftf\Helper; + +use Facebook\WebDriver\Remote\RemoteWebDriver as FacebookWebDriver; +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; + +/** + * Class for MFTF helpers for CatalogRule module. + */ +class RuleHelper extends Helper +{ + /** + * Delete all Catalog Price Rules obe by one. + * + * @param string $emptyRow + * @param string $modalAceptButton + * @param string $deleteButton + * @param string $successMessageContainer + * @param string $successMessage + * + * @return void + */ + public function deleteAllRulesOneByOne( + string $firstNotEmptyRow, + string $modalAcceptButton, + string $deleteButton, + string $successMessageContainer, + string $successMessage + ): void { + try { + /** @var MagentoWebDriver $webDriver */ + $magentoWebDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + /** @var FacebookWebDriver $webDriver */ + $webDriver = $magentoWebDriver->webDriver; + $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow)); + while (!empty($rows)) { + $rows[0]->click(); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForLoadingMaskToDisappear(); + $magentoWebDriver->waitForElementVisible($successMessageContainer, 10); + $magentoWebDriver->see($successMessage, $successMessageContainer); + $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php index 6e029ac468370..345fffc414fbc 100644 --- a/app/code/Magento/Sales/Model/Order/ItemRepository.php +++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php @@ -167,10 +167,7 @@ public function deleteById($id) public function save(OrderItemInterface $entity) { if ($entity->getProductOption()) { - $request = $this->getBuyRequest($entity); - $productOptions = $entity->getProductOptions(); - $productOptions['info_buyRequest'] = $request->toArray(); - $entity->setProductOptions($productOptions); + $entity->setProductOptions($this->getItemProductOptions($entity)); } $this->metadata->getMapper()->save($entity); @@ -178,6 +175,23 @@ public function save(OrderItemInterface $entity) return $this->registry[$entity->getEntityId()]; } + /** + * Return product options + * + * @param OrderItemInterface $entity + * @return array + */ + private function getItemProductOptions(OrderItemInterface $entity): array + { + $request = $this->getBuyRequest($entity); + $productOptions = $entity->getProductOptions(); + $productOptions['info_buyRequest'] = $productOptions && !empty($productOptions['info_buyRequest']) + ? array_merge($productOptions['info_buyRequest'], $request->toArray()) + : $request->toArray(); + + return $productOptions; + } + /** * Set parent item. * diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index 253dbd43fa580..6ddbce49829eb 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php @@ -80,11 +80,9 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [ - [ + $lines[0][] = [ 'text' => $this->string->split($this->prepareText((string)$item->getName()), 35, true, true), 'feed' => 35 - ] ]; // draw SKU diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php index 19d9b6f300eba..b1d2deb248ba1 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php @@ -43,13 +43,13 @@ public function getAllCommentCollection($orderId) $commentSelects = []; foreach (['invoice', 'shipment', 'creditmemo'] as $entityTypeCode) { $mainTable = $resource->getTableName('sales_' . $entityTypeCode); - $slaveTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment'); + $commentTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment'); $select = $read->select()->from( ['main' => $mainTable], ['entity_id' => 'order_id', 'entity_type_code' => new \Zend_Db_Expr("'{$entityTypeCode}'")] )->join( - ['slave' => $slaveTable], - 'main.entity_id = slave.parent_id', + ['comment' => $commentTable], + 'main.entity_id = comment.parent_id', $fields )->where( 'main.order_id = ?', diff --git a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html index dc3a8e9f69aca..0529c66a04d8c 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var formattedBillingAddress|raw":"Billing Address", "var order_data.email_customer_note|escape|nl2br":"Email Order Note", -"var order.billing_address.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", @@ -29,7 +29,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send an email with a link to track your order."}} diff --git a/app/code/Magento/SalesGraphQl/Model/InvoiceItemInterfaceTypeResolverComposite.php b/app/code/Magento/SalesGraphQl/Model/InvoiceItemInterfaceTypeResolverComposite.php new file mode 100644 index 0000000000000..5b3c2aee1cecf --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/InvoiceItemInterfaceTypeResolverComposite.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Composite class to resolve invoice item type + */ +class InvoiceItemInterfaceTypeResolverComposite implements TypeResolverInterface +{ + /** + * @var TypeResolverInterface[] + */ + private $invoiceItemTypeResolvers = []; + + /** + * @param TypeResolverInterface[] $invoiceItemTypeResolvers + */ + public function __construct(array $invoiceItemTypeResolvers = []) + { + $this->invoiceItemTypeResolvers = $invoiceItemTypeResolvers; + } + + /** + * Resolve item type of an invoice through composite resolvers + * + * @param array $data + * @return string + * @throws GraphQlInputException + */ + public function resolveType(array $data): string + { + $resolvedType = null; + + foreach ($this->invoiceItemTypeResolvers as $invoiceItemTypeResolver) { + if (!isset($data['product_type'])) { + throw new GraphQlInputException( + __('Missing key %1 in sales item data', ['product_type']) + ); + } + $resolvedType = $invoiceItemTypeResolver->resolveType($data); + if (!empty($resolvedType)) { + return $resolvedType; + } + } + + throw new GraphQlInputException( + __('Concrete type for %1 not implemented', ['InvoiceItemInterface']) + ); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/InvoiceItemTypeResolver.php b/app/code/Magento/SalesGraphQl/Model/InvoiceItemTypeResolver.php new file mode 100644 index 0000000000000..4c2dcdf7f29ba --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/InvoiceItemTypeResolver.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Leaf for composite class to resolve invoice item type + */ +class InvoiceItemTypeResolver implements TypeResolverInterface +{ + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (isset($data['product_type'])) { + if ($data['product_type'] == 'bundle') { + return 'BundleInvoiceItem'; + } + } + return 'InvoiceItem'; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItemInterfaceTypeResolverComposite.php b/app/code/Magento/SalesGraphQl/Model/OrderItemInterfaceTypeResolverComposite.php new file mode 100644 index 0000000000000..ed7b133ce1bb8 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/OrderItemInterfaceTypeResolverComposite.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Composite class to resolve order item type + */ +class OrderItemInterfaceTypeResolverComposite implements TypeResolverInterface +{ + /** + * TypeResolverInterface[] + */ + private $orderItemTypeResolvers = []; + + /** + * @param TypeResolverInterface[] $orderItemTypeResolvers + */ + public function __construct(array $orderItemTypeResolvers = []) + { + $this->orderItemTypeResolvers = $orderItemTypeResolvers; + } + + /** + * Resolve item type of an order through composite resolvers + * + * @param array $data + * @return string + * @throws GraphQlInputException + */ + public function resolveType(array $data) : string + { + $resolvedType = null; + + foreach ($this->orderItemTypeResolvers as $orderItemTypeResolver) { + if (!isset($data['product_type'])) { + throw new GraphQlInputException( + __('Missing key %1 in sales item data', ['product_type']) + ); + } + $resolvedType = $orderItemTypeResolver->resolveType($data); + if (!empty($resolvedType)) { + return $resolvedType; + } + } + + throw new GraphQlInputException( + __('Concrete type for %1 not implemented', ['OrderItemInterface']) + ); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItemTypeResolver.php b/app/code/Magento/SalesGraphQl/Model/OrderItemTypeResolver.php new file mode 100644 index 0000000000000..8e1b495406b54 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/OrderItemTypeResolver.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Leaf for composite class to resolve order item type + */ +class OrderItemTypeResolver implements TypeResolverInterface +{ + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (isset($data['product_type'])) { + if ($data['product_type'] == 'bundle') { + return 'BundleOrderItem'; + } + } + return 'OrderItem'; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/BundleOptions.php b/app/code/Magento/SalesGraphQl/Model/Resolver/BundleOptions.php new file mode 100644 index 0000000000000..0d27197e255ca --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/BundleOptions.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Sales\Api\Data\InvoiceItemInterface; +use Magento\Sales\Api\Data\OrderItemInterface; + +/** + * Resolve bundle options items for order item + */ +class BundleOptions implements ResolverInterface +{ + /** + * Serializer + * + * @var Json + */ + private $serializer; + + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @param ValueFactory $valueFactory + * @param Json $serializer + */ + public function __construct( + ValueFactory $valueFactory, + Json $serializer + ) { + $this->valueFactory = $valueFactory; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + return $this->valueFactory->create(function () use ($value) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + if ($value['model'] instanceof OrderItemInterface) { + /** @var OrderItemInterface $item */ + $item = $value['model']; + return $this->getBundleOptions($item, $value); + } + if ($value['model'] instanceof InvoiceItemInterface) { + /** @var InvoiceItemInterface $item */ + $item = $value['model']; + // Have to pass down order and item to map to avoid refetching all data + return $this->getBundleOptions($item->getOrderItem(), $value); + } + return null; + }); + } + + /** + * Format bundle options and values from a parent bundle order item + * + * @param OrderItemInterface $item + * @param array $formattedItem + * @return array + */ + private function getBundleOptions( + OrderItemInterface $item, + array $formattedItem + ): array { + $bundleOptions = []; + if ($item->getProductType() === 'bundle') { + $options = $item->getProductOptions(); + //loop through options + foreach ($options['bundle_options'] ?? [] as $bundleOptionId => $bundleOption) { + $bundleOptions[$bundleOptionId]['label'] = $bundleOption['label'] ?? ''; + $bundleOptions[$bundleOptionId]['id'] = isset($bundleOption['option_id']) ? + base64_encode($bundleOption['option_id']) : null; + if (isset($bundleOption['option_id'])) { + $bundleOptions[$bundleOptionId]['values'] = $this->formatBundleOptionItems( + $item, + $formattedItem, + $bundleOption['option_id'] + ); + } else { + $bundleOptions[$bundleOptionId]['values'] = []; + } + } + } + return $bundleOptions; + } + + /** + * Format Bundle items + * + * @param OrderItemInterface $item + * @param array $formattedItem + * @param string $bundleOptionId + * @return array + */ + private function formatBundleOptionItems( + OrderItemInterface $item, + array $formattedItem, + string $bundleOptionId + ) { + $optionItems = []; + // Find the item assign to the option + /** @var OrderItemInterface $childrenOrderItem */ + foreach ($item->getChildrenItems() ?? [] as $childrenOrderItem) { + $childOrderItemOptions = $childrenOrderItem->getProductOptions(); + $bundleChildAttributes = $this->serializer + ->unserialize($childOrderItemOptions['bundle_selection_attributes'] ?? ''); + // Value Id is missing from parent, so we have to match the child to parent option + if (isset($bundleChildAttributes['option_id']) + && $bundleChildAttributes['option_id'] == $bundleOptionId) { + $optionItems[$childrenOrderItem->getItemId()] = [ + 'id' => base64_encode($childrenOrderItem->getItemId()), + 'product_name' => $childrenOrderItem->getName(), + 'product_sku' => $childrenOrderItem->getSku(), + 'quantity' => $bundleChildAttributes['qty'], + 'price' => [ + //use options price, not child price + 'value' => $bundleChildAttributes['price'], + //use currency from order + 'currency' => $formattedItem['product_sale_price']['currency'] ?? null, + ] + ]; + } + } + + return $optionItems; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php new file mode 100644 index 0000000000000..30fb42a1180fc --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\SalesGraphQl\Model\Resolver\CustomerOrders\Query\OrderFilter; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Orders data resolver + */ +class CustomerOrders implements ResolverInterface +{ + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var OrderFilter + */ + private $orderFilter; + + /** + * @param OrderRepositoryInterface $orderRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param OrderFilter $orderFilter + */ + public function __construct( + OrderRepositoryInterface $orderRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + OrderFilter $orderFilter + ) { + $this->orderRepository = $orderRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->orderFilter = $orderFilter; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + if ($args['currentPage'] < 1) { + throw new GraphQlInputException(__('currentPage value must be greater than 0.')); + } + if ($args['pageSize'] < 1) { + throw new GraphQlInputException(__('pageSize value must be greater than 0.')); + } + $userId = $context->getUserId(); + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + try { + $searchResult = $this->getSearchResult($args, (int) $userId, (int)$store->getId()); + $maxPages = (int)ceil($searchResult->getTotalCount() / $searchResult->getPageSize()); + } catch (InputException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + return [ + 'total_count' => $searchResult->getTotalCount(), + 'items' => $this->formatOrdersArray($searchResult->getItems()), + 'page_info' => [ + 'page_size' => $searchResult->getPageSize(), + 'current_page' => $searchResult->getCurPage(), + 'total_pages' => $maxPages, + ] + ]; + } + + /** + * Format order models for graphql schema + * + * @param OrderInterface[] $orderModels + * @return array + */ + private function formatOrdersArray(array $orderModels) + { + $ordersArray = []; + foreach ($orderModels as $orderModel) { + $ordersArray[] = [ + 'created_at' => $orderModel->getCreatedAt(), + 'grand_total' => $orderModel->getGrandTotal(), + 'id' => base64_encode($orderModel->getEntityId()), + 'increment_id' => $orderModel->getIncrementId(), + 'number' => $orderModel->getIncrementId(), + 'order_date' => $orderModel->getCreatedAt(), + 'order_number' => $orderModel->getIncrementId(), + 'status' => $orderModel->getStatusLabel(), + 'shipping_method' => $orderModel->getShippingDescription(), + 'model' => $orderModel, + ]; + } + return $ordersArray; + } + + /** + * Get search result from graphql query arguments + * + * @param array $args + * @param int $userId + * @param int $storeId + * @return \Magento\Sales\Api\Data\OrderSearchResultInterface + * @throws InputException + */ + private function getSearchResult(array $args, int $userId, int $storeId) + { + $filterGroups = $this->orderFilter->createFilterGroups($args, $userId, (int)$storeId); + $this->searchCriteriaBuilder->setFilterGroups($filterGroups); + if (isset($args['currentPage'])) { + $this->searchCriteriaBuilder->setCurrentPage($args['currentPage']); + } + if (isset($args['pageSize'])) { + $this->searchCriteriaBuilder->setPageSize($args['pageSize']); + } + return $this->orderRepository->getList($this->searchCriteriaBuilder->create()); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php new file mode 100644 index 0000000000000..b14b05042bb4d --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CustomerOrders\Query; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Api\Search\FilterGroup; + +/** + * Order filter allows to filter collection using 'increment_id' as order number, from the search criteria. + */ +class OrderFilter +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Translator field from graphql to collection field + * + * @var string[] + */ + private $fieldTranslatorArray = [ + 'number' => 'increment_id', + ]; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param FilterBuilder $filterBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param string[] $fieldTranslatorArray + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + FilterBuilder $filterBuilder, + FilterGroupBuilder $filterGroupBuilder, + array $fieldTranslatorArray = [] + ) { + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->scopeConfig = $scopeConfig; + $this->fieldTranslatorArray = array_replace($this->fieldTranslatorArray, $fieldTranslatorArray); + } + + /** + * Create filter for filtering the requested categories id's based on url_key, ids, name in the result. + * + * @param array $args + * @param int $userId + * @param int $storeId + * @return FilterGroup[] + */ + public function createFilterGroups( + array $args, + int $userId, + int $storeId + ): array { + $filterGroups = []; + $this->filterGroupBuilder->setFilters( + [$this->filterBuilder->setField('customer_id')->setValue($userId)->setConditionType('eq')->create()] + ); + $filterGroups[] = $this->filterGroupBuilder->create(); + + $this->filterGroupBuilder->setFilters( + [$this->filterBuilder->setField('store_id')->setValue($storeId)->setConditionType('eq')->create()] + ); + $filterGroups[] = $this->filterGroupBuilder->create(); + + if (isset($args['filter'])) { + $filters = []; + foreach ($args['filter'] as $field => $cond) { + if (isset($this->fieldTranslatorArray[$field])) { + $field = $this->fieldTranslatorArray[$field]; + } + foreach ($cond as $condType => $value) { + if ($condType === 'match') { + if (is_array($value)) { + throw new InputException(__('Invalid match filter')); + } + $searchValue = str_replace('%', '', $value); + $filters[] = $this->filterBuilder->setField($field) + ->setValue("%{$searchValue}%") + ->setConditionType('like') + ->create(); + } else { + $filters[] = $this->filterBuilder->setField($field) + ->setValue($value) + ->setConditionType($condType) + ->create(); + } + } + } + + $this->filterGroupBuilder->setFilters($filters); + $filterGroups[] = $this->filterGroupBuilder->create(); + } + return $filterGroups; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/InvoiceItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/InvoiceItems.php new file mode 100644 index 0000000000000..bac9ea5480580 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/InvoiceItems.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceItemInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\SalesGraphQl\Model\Resolver\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolver for Invoice Items + */ +class InvoiceItems implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct( + ValueFactory $valueFactory, + OrderItemProvider $orderItemProvider + ) { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof InvoiceInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var InvoiceInterface $invoiceModel */ + $invoiceModel = $value['model']; + /** @var OrderInterface $parentOrderModel */ + $parentOrderModel = $value['order']; + + return $this->valueFactory->create( + $this->getInvoiceItems($parentOrderModel, $invoiceModel->getItems()) + ); + } + + /** + * Get invoice items data as promise + * + * @param OrderInterface $order + * @param array $invoiceItems + * @return \Closure + */ + public function getInvoiceItems(OrderInterface $order, array $invoiceItems): \Closure + { + $itemsList = []; + foreach ($invoiceItems as $Item) { + $this->orderItemProvider->addOrderItemId((int)$Item->getOrderItemId()); + } + return function () use ($order, $invoiceItems, $itemsList): array { + foreach ($invoiceItems as $invoiceItem) { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$invoiceItem->getOrderItemId()); + /** @var OrderItemInterface $orderItemModel */ + $orderItemModel = $orderItem['model']; + if (!$orderItemModel->getParentItem()) { + $invoiceItemData = $this->getInvoiceItemData($order, $invoiceItem); + if (isset($invoiceItemData)) { + $itemsList[$invoiceItem->getOrderItemId()] = $invoiceItemData; + } + } + } + return $itemsList; + }; + } + + /** + * Get formatted invoice item data + * + * @param OrderInterface $order + * @param InvoiceItemInterface $invoiceItem + * @return array + */ + private function getInvoiceItemData(OrderInterface $order, InvoiceItemInterface $invoiceItem): array + { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$invoiceItem->getOrderItemId()); + return [ + 'id' => base64_encode($invoiceItem->getEntityId()), + 'product_name' => $invoiceItem->getName(), + 'product_sku' => $invoiceItem->getSku(), + 'product_sale_price' => [ + 'value' => $invoiceItem->getPrice(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'quantity_invoiced' => $invoiceItem->getQty(), + 'model' => $invoiceItem, + 'product_type' => $orderItem['product_type'] + ]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/InvoiceTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/InvoiceTotal.php new file mode 100644 index 0000000000000..45752c5f807b8 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/InvoiceTotal.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Resolver for Invoice total + */ +class InvoiceTotal implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof InvoiceInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['order']; + /** @var InvoiceInterface $invoiceModel */ + $invoiceModel = $value['model']; + $currency = $orderModel->getOrderCurrencyCode(); + return [ + 'base_grand_total' => ['value' => $invoiceModel->getBaseGrandTotal(), 'currency' => $currency], + 'grand_total' => ['value' => $invoiceModel->getGrandTotal(), 'currency' => $currency], + 'subtotal' => ['value' => $invoiceModel->getSubtotal(), 'currency' => $currency], + 'total_tax' => ['value' => $invoiceModel->getTaxAmount(), 'currency' => $currency], + 'total_shipping' => ['value' => $invoiceModel->getShippingAmount(), 'currency' => $currency], + 'shipping_handling' => [ + 'amount_excluding_tax' => [ + 'value' => $invoiceModel->getShippingAmount(), + 'currency' => $currency + ], + 'amount_including_tax' => [ + 'value' => $invoiceModel->getShippingInclTax(), + 'currency' => $currency + ], + 'total_amount' => [ + 'value' => $invoiceModel->getShippingAmount(), + 'currency' => $currency + ], + ] + ]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php new file mode 100644 index 0000000000000..f106752075c25 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\InvoiceInterface; + +/** + * Resolver for Invoice + */ +class Invoices implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['model']; + $invoices = []; + /** @var InvoiceInterface $invoice */ + foreach ($orderModel->getInvoiceCollection() as $invoice) { + $invoices[] = [ + 'id' => base64_encode($invoice->getEntityId()), + 'number' => $invoice['increment_id'], + 'model' => $invoice, + 'order' => $orderModel + ]; + } + return $invoices; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php new file mode 100644 index 0000000000000..116066f12bc28 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\SalesGraphQl\Model\Resolver\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolve a single order item + */ +class OrderItem implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct(ValueFactory $valueFactory, OrderItemProvider $orderItemProvider) + { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $parentItem = $value['model']; + + if (!method_exists($parentItem, 'getOrderItemId')) { + throw new LocalizedException(__('Unable to find associated order item.')); + } + + $orderItemId = $parentItem->getOrderItemId(); + $this->orderItemProvider->addOrderItemId((int)$orderItemId); + + return $this->valueFactory->create(function () use ($parentItem) { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$parentItem->getOrderItemId()); + return empty($orderItem) ? null : $orderItem; + }); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/DataProvider.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/DataProvider.php new file mode 100644 index 0000000000000..20cdd7313b8ad --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/DataProvider.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\OrderItem; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; + +/** + * Data provider for order items + */ +class DataProvider +{ + /** + * @var OrderItemRepositoryInterface + */ + private $orderItemRepository; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var OptionsProcessor + */ + private $optionsProcessor; + + /** + * @var int[] + */ + private $orderItemIds = []; + + /** + * @var array + */ + private $orderItemList = []; + + /** + * @param OrderItemRepositoryInterface $orderItemRepository + * @param ProductRepositoryInterface $productRepository + * @param OrderRepositoryInterface $orderRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param OptionsProcessor $optionsProcessor + */ + public function __construct( + OrderItemRepositoryInterface $orderItemRepository, + ProductRepositoryInterface $productRepository, + OrderRepositoryInterface $orderRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + OptionsProcessor $optionsProcessor + ) { + $this->orderItemRepository = $orderItemRepository; + $this->productRepository = $productRepository; + $this->orderRepository = $orderRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->optionsProcessor = $optionsProcessor; + } + + /** + * Add order item id to list for fetching + * + * @param int $orderItemId + */ + public function addOrderItemId(int $orderItemId): void + { + if (!in_array($orderItemId, $this->orderItemIds)) { + $this->orderItemList = []; + $this->orderItemIds[] = $orderItemId; + } + } + + /** + * Get order item by item id + * + * @param int $orderItemId + * @return array + */ + public function getOrderItemById(int $orderItemId): array + { + $orderItems = $this->fetch(); + if (!isset($orderItems[$orderItemId])) { + return []; + } + return $orderItems[$orderItemId]; + } + + /** + * Fetch order items and return in format for GraphQl + * + * @return array + */ + private function fetch() + { + if (empty($this->orderItemIds) || !empty($this->orderItemList)) { + return $this->orderItemList; + } + + $itemSearchCriteria = $this->searchCriteriaBuilder + ->addFilter(OrderItemInterface::ITEM_ID, $this->orderItemIds, 'in') + ->create(); + + $orderItems = $this->orderItemRepository->getList($itemSearchCriteria)->getItems(); + $productList = $this->fetchProducts($orderItems); + $orderList = $this->fetchOrders($orderItems); + + foreach ($orderItems as $orderItem) { + /** @var ProductInterface $associatedProduct */ + $associatedProduct = $productList[$orderItem->getProductId()] ?? null; + /** @var OrderInterface $associatedOrder */ + $associatedOrder = $orderList[$orderItem->getOrderId()]; + $itemOptions = $this->optionsProcessor->getItemOptions($orderItem); + $this->orderItemList[$orderItem->getItemId()] = [ + 'id' => base64_encode($orderItem->getItemId()), + 'associatedProduct' => $associatedProduct, + 'model' => $orderItem, + 'product_name' => $orderItem->getName(), + 'product_sku' => $orderItem->getSku(), + 'product_url_key' => $associatedProduct ? $associatedProduct->getUrlKey() : null, + 'product_type' => $orderItem->getProductType(), + 'status' => $orderItem->getStatus(), + 'discounts' => $this->getDiscountDetails($associatedOrder, $orderItem), + 'product_sale_price' => [ + 'value' => $orderItem->getPrice(), + 'currency' => $associatedOrder->getOrderCurrencyCode() + ], + 'selected_options' => $itemOptions['selected_options'], + 'entered_options' => $itemOptions['entered_options'], + 'quantity_ordered' => $orderItem->getQtyOrdered(), + 'quantity_shipped' => $orderItem->getQtyShipped(), + 'quantity_refunded' => $orderItem->getQtyRefunded(), + 'quantity_invoiced' => $orderItem->getQtyInvoiced(), + 'quantity_canceled' => $orderItem->getQtyCanceled(), + 'quantity_returned' => $orderItem->getQtyReturned() + ]; + } + + return $this->orderItemList; + } + + /** + * Fetch associated products for order items + * + * @param array $orderItems + * @return array + */ + private function fetchProducts(array $orderItems): array + { + $productIds = array_map( + function ($orderItem) { + return $orderItem->getProductId(); + }, + $orderItems + ); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $productIds, 'in') + ->create(); + $products = $this->productRepository->getList($searchCriteria)->getItems(); + $productList = []; + foreach ($products as $product) { + $productList[$product->getId()] = $product; + } + return $productList; + } + + /** + * Fetch associated order for order items + * + * @param array $orderItems + * @return array + */ + private function fetchOrders(array $orderItems): array + { + $orderIds = array_map( + function ($orderItem) { + return $orderItem->getOrderId(); + }, + $orderItems + ); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $orderIds, 'in') + ->create(); + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + + $orderList = []; + foreach ($orders as $order) { + $orderList[$order->getEntityId()] = $order; + } + return $orderList; + } + + /** + * Returns information about an applied discount + * + * @param OrderInterface $associatedOrder + * @param OrderItemInterface $orderItem + * @return array + */ + private function getDiscountDetails(OrderInterface $associatedOrder, OrderItemInterface $orderItem) : array + { + if ($associatedOrder->getDiscountDescription() === null && $orderItem->getDiscountAmount() == 0 + && $associatedOrder->getDiscountAmount() == 0 + ) { + $discounts = []; + } else { + $discounts [] = [ + 'label' => $associatedOrder->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($orderItem->getDiscountAmount()) ?? 0, + 'currency' => $associatedOrder->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/OptionsProcessor.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/OptionsProcessor.php new file mode 100644 index 0000000000000..e168f185d39a4 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem/OptionsProcessor.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\OrderItem; + +use Magento\Sales\Api\Data\OrderItemInterface; + +/** + * Process order item options to format for GraphQl output + */ +class OptionsProcessor +{ + /** + * Get Order item options. + * + * @param OrderItemInterface $orderItem + * @return array + */ + public function getItemOptions(OrderItemInterface $orderItem): array + { + //build options array + $optionsTypes = ['selected_options' => [], 'entered_options' => []]; + $options = $orderItem->getProductOptions(); + if ($options) { + if (isset($options['options'])) { + $optionsTypes = $this->processOptions($options['options']); + } elseif (isset($options['attributes_info'])) { + $optionsTypes = $this->processAttributesInfo($options['attributes_info']); + } + } + return $optionsTypes; + } + + /** + * Process options data + * + * @param array $options + * @return array + */ + private function processOptions(array $options): array + { + $selectedOptions = []; + $enteredOptions = []; + foreach ($options ?? [] as $option) { + if (isset($option['option_type'])) { + if (in_array($option['option_type'], ['field', 'area', 'file', 'date', 'date_time', 'time'])) { + $selectedOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } elseif (in_array($option['option_type'], ['drop_down', 'radio', 'checkbox', 'multiple'])) { + $enteredOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } + } + } + return ['selected_options' => $selectedOptions, 'entered_options' => $enteredOptions]; + } + + /** + * Process attributes info data + * + * @param array $attributesInfo + * @return array + */ + private function processAttributesInfo(array $attributesInfo): array + { + $selectedOptions = []; + foreach ($attributesInfo ?? [] as $option) { + $selectedOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } + return ['selected_options' => $selectedOptions, 'entered_options' => []]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php new file mode 100644 index 0000000000000..29e03afa9b59a --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\SalesGraphQl\Model\Resolver\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolve order items for order + */ +class OrderItems implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct( + ValueFactory $valueFactory, + OrderItemProvider $orderItemProvider + ) { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + /** @var ContextInterface $context */ + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var OrderInterface $parentOrder */ + $parentOrder = $value['model']; + $orderItemIds = []; + foreach ($parentOrder->getItems() as $item) { + if (!$item->getParentItemId()) { + $orderItemIds[] = (int)$item->getItemId(); + } + $this->orderItemProvider->addOrderItemId((int)$item->getItemId()); + } + $itemsList = []; + foreach ($orderItemIds as $orderItemId) { + $itemsList[] = $this->valueFactory->create( + function () use ($orderItemId) { + return $this->orderItemProvider->getOrderItemById((int)$orderItemId); + } + ); + } + return $itemsList; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php new file mode 100644 index 0000000000000..6f7b943bf6ca2 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Resolve order totals taxes and discounts for order + */ +class OrderTotal implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var OrderInterface $order */ + $order = $value['model']; + $currency = $order->getOrderCurrencyCode(); + + return [ + 'base_grand_total' => ['value' => $order->getBaseGrandTotal(), 'currency' => $currency], + 'grand_total' => ['value' => $order->getGrandTotal(), 'currency' => $currency], + 'subtotal' => ['value' => $order->getSubtotal(), 'currency' => $currency], + 'total_tax' => ['value' => $order->getTaxAmount(), 'currency' => $currency], + 'taxes' => $this->getAppliedTaxesDetails($order), + 'discounts' => $this->getDiscountDetails($order), + 'total_shipping' => ['value' => $order->getShippingAmount(), 'currency' => $currency], + 'shipping_handling' => [ + 'amount_excluding_tax' => [ + 'value' => $order->getShippingAmount(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'amount_including_tax' => [ + 'value' => $order->getShippingInclTax(), + 'currency' => $currency + ], + 'total_amount' => [ + 'value' => $order->getShippingAmount(), + 'currency' => $currency + ], + 'taxes' => $this->getAppliedShippingTaxesDetails($order), + 'discounts' => $this->getShippingDiscountDetails($order), + ] + ]; + } + + /** + * Retrieve applied taxes that apply to the order + * + * @param OrderInterface $order + * @return array + */ + private function getAllAppliedTaxesOnOrders(OrderInterface $order): array + { + $extensionAttributes = $order->getExtensionAttributes(); + $appliedTaxes = $extensionAttributes->getAppliedTaxes() ?? []; + $allAppliedTaxOnOrders = []; + foreach ($appliedTaxes as $taxIndex => $appliedTaxesData) { + $allAppliedTaxOnOrders[$taxIndex] = [ + 'title' => $appliedTaxesData->getDataByKey('title'), + 'percent' => $appliedTaxesData->getDataByKey('percent'), + 'amount' => $appliedTaxesData->getDataByKey('amount'), + ]; + } + return $allAppliedTaxOnOrders; + } + + /** + * Return taxes applied to the current order + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedTaxesDetails(OrderInterface $order): array + { + $allAppliedTaxOnOrders = $this->getAllAppliedTaxesOnOrders($order); + $taxes = []; + foreach ($allAppliedTaxOnOrders as $appliedTaxes) { + $appliedTaxesArray = [ + 'rate' => $appliedTaxes['percent'] ?? 0, + 'title' => $appliedTaxes['title'] ?? null, + 'amount' => [ + 'value' => $appliedTaxes['amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $taxes[] = $appliedTaxesArray; + } + return $taxes; + } + + /** + * Return information about an applied discount + * + * @param OrderInterface $order + * @return array + */ + private function getDiscountDetails(OrderInterface $order): array + { + $orderDiscounts = []; + if (!($order->getDiscountDescription() === null && $order->getDiscountAmount() == 0)) { + $orderDiscounts[] = [ + 'label' => $order->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($order->getDiscountAmount()), + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + } + return $orderDiscounts; + } + + /** + * Retrieve applied shipping taxes on items for the orders + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedShippingTaxesForItems(OrderInterface $order): array + { + $extensionAttributes = $order->getExtensionAttributes(); + $itemAppliedTaxes = $extensionAttributes->getItemAppliedTaxes() ?? []; + $appliedShippingTaxesForItems = []; + foreach ($itemAppliedTaxes as $appliedTaxForItem) { + if ($appliedTaxForItem->getType() === "shipping") { + foreach ($appliedTaxForItem->getAppliedTaxes() ?? [] as $taxLineItem) { + $taxItemIndexTitle = $taxLineItem->getDataByKey('title'); + $appliedShippingTaxesForItems[$taxItemIndexTitle] = [ + 'title' => $taxLineItem->getDataByKey('title'), + 'percent' => $taxLineItem->getDataByKey('percent'), + 'amount' => $taxLineItem->getDataByKey('amount') + ]; + } + } + } + return $appliedShippingTaxesForItems; + } + + /** + * Return taxes applied to the current order + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedShippingTaxesDetails( + OrderInterface $order + ): array { + $appliedShippingTaxesForItems = $this->getAppliedShippingTaxesForItems($order); + $shippingTaxes = []; + foreach ($appliedShippingTaxesForItems as $appliedShippingTaxes) { + $appliedShippingTaxesArray = [ + 'rate' => $appliedShippingTaxes['percent'] ?? 0, + 'title' => $appliedShippingTaxes['title'] ?? null, + 'amount' => [ + 'value' => $appliedShippingTaxes['amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $shippingTaxes[] = $appliedShippingTaxesArray; + } + return $shippingTaxes; + } + + /** + * Return information about an applied discount + * + * @param OrderInterface $order + * @return array + */ + private function getShippingDiscountDetails(OrderInterface $order): array + { + $shippingDiscounts = []; + if (!($order->getDiscountDescription() === null && $order->getShippingDiscountAmount() == 0)) { + $shippingDiscounts[] = + [ + 'label' => $order->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($order->getShippingDiscountAmount()), + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + } + return $shippingDiscounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php index 8d81afeab4c90..25a79fa5d3b6c 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php @@ -12,6 +12,7 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Sales\Model\Order; use Magento\Sales\Model\ResourceModel\Order\CollectionFactoryInterface; /** @@ -34,7 +35,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function resolve( Field $field, @@ -51,7 +52,7 @@ public function resolve( $items = []; $orders = $this->collectionFactory->create($context->getUserId()); - /** @var \Magento\Sales\Model\Order $order */ + /** @var Order $order */ foreach ($orders as $order) { $items[] = [ 'id' => $order->getId(), diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php index 8bf4220d1ec3d..70c411c379b62 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php @@ -49,7 +49,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function resolve( Field $field, diff --git a/app/code/Magento/SalesGraphQl/composer.json b/app/code/Magento/SalesGraphQl/composer.json index 8e9d95836e189..9fd6e76220df3 100644 --- a/app/code/Magento/SalesGraphQl/composer.json +++ b/app/code/Magento/SalesGraphQl/composer.json @@ -6,8 +6,13 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-catalog": "*", "magento/module-graph-ql": "*" }, + "suggest": { + "magento/module-shipping": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SalesGraphQl/etc/graphql/di.xml b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..5bba224ff2fad --- /dev/null +++ b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml @@ -0,0 +1,23 @@ +<?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:ObjectManager/etc/config.xsd"> + <type name="Magento\SalesGraphQl\Model\OrderItemInterfaceTypeResolverComposite"> + <arguments> + <argument name="orderItemTypeResolvers" xsi:type="array"> + <item name="order_catalog_item_type_resolver" xsi:type="object">Magento\SalesGraphQl\Model\OrderItemTypeResolver</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\InvoiceItemInterfaceTypeResolverComposite"> + <arguments> + <argument name="invoiceItemTypeResolvers" xsi:type="array"> + <item name="invoice_catalog_item_type_resolver" xsi:type="object">Magento\SalesGraphQl\Model\InvoiceItemTypeResolver</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index f823c25cf2d9f..099a3ffb959c4 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -2,20 +2,7 @@ # See COPYING.txt for license details. type Query { - customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @doc(description: "List of customer orders") @cache(cacheable: false) -} - -type CustomerOrder @doc(description: "Order mapping fields") { - id: Int - increment_id: String @deprecated(reason: "Use the order_number instead.") - order_number: String! @doc(description: "The order number") - created_at: String - grand_total: Float - status: String -} - -type CustomerOrders { - items: [CustomerOrder] @doc(description: "Array of orders") + customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @deprecated(reason: "Use orders from customer instead") @cache(cacheable: false) } type Mutation { @@ -33,6 +20,190 @@ type CheckoutUserInputError @doc(description:"An error encountered while adding code: CheckoutUserInputErrorCodes! @doc(description: "Checkout-specific error code") } +type Customer { + orders ( + filter: CustomerOrdersFilterInput @doc(description: "Defines the filter to use for searching customer orders"), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1"), + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default value is 20"), + ): CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CustomerOrders") @cache(cacheable: false) +} + +input CustomerOrdersFilterInput @doc(description: "Identifies the filter to use for filtering orders.") { + number: FilterStringTypeInput @doc(description: "Filters by order number.") +} + +type CustomerOrders @doc(description: "The collection of orders that match the conditions defined in the filter") { + items: [CustomerOrder]! @doc(description: "An array of customer orders") + page_info: SearchResultPageInfo @doc(description: "An object that includes the current_page, page_info, and page_size values specified in the query") + total_count: Int @doc(description: "The total count of customer orders") +} + +type CustomerOrder @doc(description: "Contains details about each of the customer's orders") { + id: ID! @doc(description: "Unique identifier for the order") + order_date: String! @doc(description: "The date the order was placed") + status: String! @doc(description: "The current status of the order") + number: String! @doc(description: "The order number") + items: [OrderItemInterface] @doc(description: "An array containing the items purchased in this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItems") + total: OrderTotal @doc(description: "Contains details about the calculated totals for this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderTotal") + invoices: [Invoice]! @doc(description: "A list of invoices for the order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoices") + shipments: [OrderShipment] @doc(description: "A list of shipments for the order") + payment_methods: [PaymentMethod] @doc(description: "Payment details for the order") + shipping_address: CustomerAddress @doc(description: "The shipping address for the order") + billing_address: CustomerAddress @doc(description: "The billing address for the order") + carrier: String @doc(description: "The shipping carrier for the order delivery") + shipping_method: String @doc(description: "The delivery method for the order") + comments: [CommentItem] @doc(description: "Comments about the order") + increment_id: String @deprecated(reason: "Use the id attribute instead") + order_number: String! @deprecated(reason: "Use the number attribute instead") + created_at: String @deprecated(reason: "Use the order_date attribute instead") + grand_total: Float @deprecated(reason: "Use the totals.grand_total attribute instead") +} + +interface OrderItemInterface @doc(description: "Order item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\OrderItemTypeResolver") { + id: ID! @doc(description: "The unique identifier of the order item") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "The SKU of the base product") + product_url_key: String @doc(description: "URL key of the base product") + product_type: String @doc(description: "The type of product, such as simple, configurable, or bundle") + status: String @doc(description: "The status of the order item") + product_sale_price: Money! @doc(description: "The sale price of the base product, including selected options") + discounts: [Discount] @doc(description: "The final discount information for the product") + selected_options: [OrderItemOption] @doc(description: "The selected options for the base product, such as color or size") + entered_options: [OrderItemOption] @doc(description: "The entered option for the base product, such as a logo or image") + quantity_ordered: Float @doc(description: "The number of units ordered for this item") + quantity_shipped: Float @doc(description: "The number of shipped items") + quantity_refunded: Float @doc(description: "The number of refunded items") + quantity_invoiced: Float @doc(description: "The number of invoiced items") + quantity_canceled: Float @doc(description: "The number of canceled items") + quantity_returned: Float @doc(description: "The number of returned items") +} + +type OrderItem implements OrderItemInterface { +} + +type BundleOrderItem implements OrderItemInterface { + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\BundleOptions") +} + +type ItemSelectedBundleOption @doc(description: "A list of options of the selected bundle product") { + id: ID! @doc(description: "The unique identifier of the option") + label: String! @doc(description: "The label of the option") + values: [ItemSelectedBundleOptionValue] @doc(description: "A list of products that represent the values of the parent option") +} + +type ItemSelectedBundleOptionValue @doc(description: "A list of values for the selected bundle product") { + id: ID! @doc(description: "The unique identifier of the value") + product_name: String! @doc(description: "The name of the child bundle product") + product_sku: String! @doc(description: "The SKU of the child bundle product") + quantity: Float! @doc(description: "Indicates how many of this bundle product were ordered") + price: Money! @doc(description: "The price of the child bundle product") +} + +type OrderItemOption @doc(description: "Represents order item options like selected or entered") { + id: String! @doc(description: "The name of the option") + value: String! @doc(description: "The value of the option") +} + +type TaxItem @doc(description: "The tax item details") { + amount: Money! @doc(description: "The amount of tax applied to the item") + title: String! @doc(description: "A title that describes the tax") + rate: Float! @doc(description: "The rate used to calculate the tax") +} + +type OrderTotal @doc(description: "Contains details about the sales total amounts used to calculate the final price") { + subtotal: Money! @doc(description: "The subtotal of the order, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the order") + total_tax: Money! @doc(description: "The amount of tax applied to the order") + taxes: [TaxItem] @doc(description: "The order tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the order") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the order") +} + +type Invoice @doc(description: "Invoice details") { + id: ID! @doc(description: "The ID of the invoice, used for API purposes") + number: String! @doc(description: "Sequential invoice number") + total: InvoiceTotal @doc(description: "Invoice total amount details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\InvoiceTotal") + items: [InvoiceItemInterface] @doc(description: "Invoiced product details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\InvoiceItems") + comments: [CommentItem] @doc(description: "Comments on the invoice") +} + +interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\InvoiceItemTypeResolver") { + id: ID! @doc(description: "The unique ID of the invoice item") + order_item: OrderItemInterface @doc(description: "Contains details about an individual order item") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "The SKU of the base product") + product_sale_price: Money! @doc(description: "The sale price for the base product including selected options") + discounts: [Discount] @doc(description: "Contains information about the final discount amount for the base product, including discounts on options") + quantity_invoiced: Float @doc(description: "The number of invoiced items") +} + +type InvoiceItem implements InvoiceItemInterface { +} + +type BundleInvoiceItem implements InvoiceItemInterface{ + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\BundleOptions") +} + +type InvoiceTotal @doc(description: "Contains price details from an invoice"){ + subtotal: Money! @doc(description: "The subtotal of the invoice, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the invoice") + total_tax: Money! @doc(description: "The amount of tax applied to the invoice") + taxes: [TaxItem] @doc(description: "The order tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the invoice") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the invoice") +} + +type ShippingHandling @doc(description: "The Shipping handling details") { + total_amount: Money! @doc(description: "The total amount for shipping") + amount_including_tax: Money @doc(description: "The shipping amount, including tax") + amount_excluding_tax: Money @doc(description: "The shipping amount, excluding tax") + taxes: [TaxItem] @doc(description: "Contains details about taxes applied for shipping") + discounts: [Discount] @doc(description: "The applied discounts to the shipping") +} + +type OrderShipment @doc(description: "Order shipment details") { + id: ID! @doc(description: "The unique ID of the shipment") + number: String! @doc(description: "The sequential credit shipment number") + tracking: [ShipmentTracking] @doc(description: "Contains shipment tracking details") + items: [ShipmentItem] @doc(description: "Contains items included in the shipment") + comments: [CommentItem] @doc(description: "Comments added to the shipment") +} + +type CommentItem @doc(description: "Comment item details") { + timestamp: String! @doc(description: "The timestamp of the comment") + message: String! @doc(description: "The text of the message") +} + +type ShipmentItem @doc(description: "Order shipment item details") { + id: ID! @doc(description: "The unique ID of the shipment item") + order_item: OrderItemInterface @doc(description: "The shipped order item") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "The SKU of the base product") + product_sale_price: Money! @doc(description: "The sale price for the base product") + quantity_shipped: Float! @doc(description: "The number of shipped items") +} + +type ShipmentTracking @doc(description: "Order shipment tracking details") { + title: String! @doc(description: "The shipment tracking title") + carrier: String! @doc(description: "The shipping carrier for the order delivery") + number: String @doc(description: "The tracking number of the order shipment") +} + +type PaymentMethod @doc(description: "Contains details about the payment method used to pay for the order") { + name: String! @doc(description: "The label that describes the payment method") + type: String! @doc(description: "The payment method code that indicates how the order was paid for") + additional_data: [KeyValue] @doc(description: "Additional data per payment method type") +} + +type KeyValue @doc(description: "The key-value type") { + name: String @doc(description: "The name part of the name/value pair") + value: String @doc(description: "The value part of the name/value pair") +} + enum CheckoutUserInputErrorCodes { REORDER_NOT_AVAILABLE PRODUCT_NOT_FOUND diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml new file mode 100644 index 0000000000000..85437650efc35 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml @@ -0,0 +1,29 @@ +<?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="AdminCartPriceRuleDeleteAllActionGroup"> + <annotations> + <description>Open Cart Price Rule grid and delete all rules one by one. Need to avoid interference with other tests that test cart price rules.</description> + </annotations> + + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="goToAdminCartPriceRuleGridPage"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <helper class="\Magento\Rule\Test\Mftf\Helper\RuleHelper" method="deleteAllRulesOneByOne" stepKey="deleteAllRulesOneByOne"> + <argument name="firstNotEmptyRow">{{AdminDataGridTableSection.firstNotEmptyRow}}</argument> + <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> + <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> + <argument name="successMessageContainer">{{AdminMessagesSection.success}}</argument> + <argument name="successMessage">You deleted the rule.</argument> + </helper> + <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js index 13b701c6fe65a..21f49fb3080fc 100644 --- a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js +++ b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js @@ -8,6 +8,9 @@ var config = { mixins: { 'Magento_Checkout/js/action/select-payment-method': { 'Magento_SalesRule/js/action/select-payment-method-mixin': true + }, + 'Magento_Checkout/js/model/shipping-save-processor': { + 'Magento_SalesRule/js/model/shipping-save-processor-mixin': true } } } diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js new file mode 100644 index 0000000000000..193acb8eed2f4 --- /dev/null +++ b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'mage/utils/wrapper', + 'Magento_Checkout/js/model/quote', + 'Magento_SalesRule/js/model/coupon' +], function (wrapper, quote, coupon) { + 'use strict'; + + return function (shippingSaveProcessor) { + shippingSaveProcessor.saveShippingInformation = wrapper.wrapSuper( + shippingSaveProcessor.saveShippingInformation, + function (type) { + var updateCouponCallback; + + /** + * Update coupon form + */ + updateCouponCallback = function () { + if (quote.totals() && !quote.totals()['coupon_code']) { + coupon.setCouponCode(''); + coupon.setIsApplied(false); + } + }; + + return this._super(type).done(updateCouponCallback); + } + ); + + return shippingSaveProcessor; + }; +}); diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md index c71439b929013..e0666ba73fe24 100644 --- a/app/code/Magento/SampleData/README.md +++ b/app/code/Magento/SampleData/README.md @@ -11,7 +11,7 @@ You can deploy sample data from one of the following sources: * From the Magento composer repository, optionally using Magento CLI * From the Magento GitHub repository -If your Magento code base was cloned from the `master` branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch. +If your Magento code base was cloned from the mainline branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch. ### Deploy Sample Data from Composer Repository @@ -46,7 +46,7 @@ Each package corresponds to a sample data module. The complete list of available To deploy sample data from the GitHub repository: -1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the `master` branch, choose the `master` branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`. +1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the mainline branch, choose the mainline branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`. 2. Link the sample data and your Magento instance by running: `# php -f <sample-data_clone_dir>/dev/tools/build-sample-data.php -- --ce-source="<path_to_your_magento_instance>"` ## Install Sample Data diff --git a/app/code/Magento/Search/Model/SearchEngine/Validator.php b/app/code/Magento/Search/Model/SearchEngine/Validator.php index f4fc8a9a62e0e..264e7c69dd520 100644 --- a/app/code/Magento/Search/Model/SearchEngine/Validator.php +++ b/app/code/Magento/Search/Model/SearchEngine/Validator.php @@ -22,7 +22,7 @@ class Validator implements ValidatorInterface /** * @var array */ - private $engineBlacklist = ['mysql' => 'MySQL']; + private $excludedEngineList = ['mysql' => 'MySQL']; /** * @var ValidatorInterface[] @@ -32,16 +32,16 @@ class Validator implements ValidatorInterface /** * @param ScopeConfigInterface $scopeConfig * @param array $engineValidators - * @param array $engineBlacklist + * @param array $excludedEngineList */ public function __construct( ScopeConfigInterface $scopeConfig, array $engineValidators = [], - array $engineBlacklist = [] + array $excludedEngineList = [] ) { $this->scopeConfig = $scopeConfig; $this->engineValidators = $engineValidators; - $this->engineBlacklist = array_merge($this->engineBlacklist, $engineBlacklist); + $this->excludedEngineList = array_merge($this->excludedEngineList, $excludedEngineList); } /** @@ -51,9 +51,9 @@ public function validate(): array { $errors = []; $currentEngine = $this->scopeConfig->getValue('catalog/search/engine'); - if (isset($this->engineBlacklist[$currentEngine])) { - $blacklistedEngine = $this->engineBlacklist[$currentEngine]; - $errors[] = "Your current search engine, '{$blacklistedEngine}', is not supported." + if (isset($this->excludedEngineList[$currentEngine])) { + $excludedEngine = $this->excludedEngineList[$currentEngine]; + $errors[] = "Your current search engine, '{$excludedEngine}', is not supported." . " You must install a supported search engine before upgrading." . " See the System Upgrade Guide for more information."; } diff --git a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php index c91c0fce9dd47..cc272ccb60162 100644 --- a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php @@ -34,7 +34,7 @@ protected function setUp(): void [ 'scopeConfig' => $this->scopeConfigMock, 'engineValidators' => ['otherEngine' => $this->otherEngineValidatorMock], - 'engineBlacklist' => ['badEngine' => 'Bad Engine'] + 'excludedEngineList' => ['badEngine' => 'Bad Engine'] ] ); } @@ -54,7 +54,7 @@ public function testValidateValid() $this->assertEquals($expectedErrors, $this->validator->validate()); } - public function testValidateBlacklist() + public function testValidateExcludedList() { $this->scopeConfigMock ->expects($this->once()) diff --git a/app/code/Magento/Security/Model/Plugin/Auth.php b/app/code/Magento/Security/Model/Plugin/Auth.php index 833b4e4c1b774..b388ef6115867 100644 --- a/app/code/Magento/Security/Model/Plugin/Auth.php +++ b/app/code/Magento/Security/Model/Plugin/Auth.php @@ -35,6 +35,8 @@ public function __construct( } /** + * Add warning message if other sessions terminated + * * @param \Magento\Backend\Model\Auth $authModel * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -43,11 +45,13 @@ public function afterLogin(\Magento\Backend\Model\Auth $authModel) { $this->sessionsManager->processLogin(); if ($this->sessionsManager->getCurrentSession()->isOtherSessionsTerminated()) { - $this->messageManager->addWarning(__('All other open sessions for this account were terminated.')); + $this->messageManager->addWarningMessage(__('All other open sessions for this account were terminated.')); } } /** + * Handle logout process + * * @param \Magento\Backend\Model\Auth $authModel * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php index c431f1ecda332..dd86b3b574ead 100644 --- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php @@ -64,7 +64,7 @@ protected function setUp(): void $this->messageManager = $this->getMockForAbstractClass( ManagerInterface::class, - ['addWarning'], + ['addWarningMessage'], '', false ); @@ -100,7 +100,7 @@ public function testAfterLogin() ->method('isOtherSessionsTerminated') ->willReturn(true); $this->messageManager->expects($this->once()) - ->method('addWarning') + ->method('addWarningMessage') ->with($warningMessage); $this->model->afterLogin($this->authMock); diff --git a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php index 537fec4c75df6..8f6011f1ae56f 100644 --- a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php +++ b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php @@ -6,7 +6,7 @@ namespace Magento\Store\Api\Data; /** - * StoreConfig interface + * Interface for store config * * @api * @since 100.0.2 @@ -141,7 +141,7 @@ public function setWeightUnit($weightUnit); public function getBaseUrl(); /** - * set base URL + * Set base URL * * @param string $baseUrl * @return $this @@ -201,7 +201,7 @@ public function setBaseMediaUrl($baseMediaUrl); public function getSecureBaseUrl(); /** - * set secure base URL + * Set secure base URL * * @param string $secureBaseUrl * @return $this diff --git a/app/code/Magento/Store/Model/Data/StoreConfig.php b/app/code/Magento/Store/Model/Data/StoreConfig.php index 6634e2cb05bd9..e68d98b162613 100644 --- a/app/code/Magento/Store/Model/Data/StoreConfig.php +++ b/app/code/Magento/Store/Model/Data/StoreConfig.php @@ -6,7 +6,7 @@ namespace Magento\Store\Model\Data; /** - * Class StoreConfig + * Allows to get and set store config values * * @codeCoverageIgnore */ @@ -188,7 +188,7 @@ public function getBaseUrl() } /** - * set base URL + * Set base URL * * @param string $baseUrl * @return $this @@ -293,7 +293,7 @@ public function getSecureBaseUrl() } /** - * set secure base URL + * Set secure base URL * * @param string $secureBaseUrl * @return $this @@ -367,7 +367,7 @@ public function setSecureBaseMediaUrl($secureBaseMediaUrl) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Store\Api\Data\StoreConfigExtensionInterface|null */ @@ -377,7 +377,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Store\Api\Data\StoreConfigExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Store/Model/Service/StoreConfigManager.php b/app/code/Magento/Store/Model/Service/StoreConfigManager.php index b3c2208a58361..debb08438a3b4 100644 --- a/app/code/Magento/Store/Model/Service/StoreConfigManager.php +++ b/app/code/Magento/Store/Model/Service/StoreConfigManager.php @@ -5,6 +5,9 @@ */ namespace Magento\Store\Model\Service; +/** + * Allows to get store config + */ class StoreConfigManager implements \Magento\Store\Api\StoreConfigManagerInterface { /** @@ -53,6 +56,8 @@ public function __construct( } /** + * Get store configs + * * @param string[] $storeCodes list of stores by store codes, will return all if storeCodes is not set * @return \Magento\Store\Api\Data\StoreConfigInterface[] */ @@ -71,6 +76,8 @@ public function getStoreConfigs(array $storeCodes = null) } /** + * Get store config + * * @param \Magento\Store\Model\Store $store * @return \Magento\Store\Api\Data\StoreConfigInterface */ diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml new file mode 100644 index 0000000000000..4a403364a91e3 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.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="StorefrontSwitchStoreActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="storeName" type="string"/> + </arguments> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickOnSwitchStoreButton"/> + <click selector="{{StorefrontFooterSection.storeLink(storeName)}}" stepKey="selectStoreToSwitchOn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 5bd8f6e2349fc..2da9e91e1fddd 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -65,7 +65,6 @@ <preference for="Magento\Framework\App\Router\PathConfigInterface" type="Magento\Store\Model\PathConfig" /> <type name="Magento\Framework\App\ActionInterface"> <plugin name="storeCheck" type="Magento\Store\App\Action\Plugin\StoreCheck"/> - <plugin name="designLoader" type="Magento\Framework\App\Action\Plugin\LoadDesignPlugin"/> <plugin name="eventDispatch" type="Magento\Framework\App\Action\Plugin\EventDispatchPlugin"/> <plugin name="actionFlagNoDispatch" type="Magento\Framework\App\Action\Plugin\ActionFlagNoDispatchPlugin"/> </type> diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php index 59f9831789a35..0baee00f468a0 100644 --- a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php @@ -55,11 +55,10 @@ public function __construct( */ public function getStoreConfigData(StoreInterface $store): array { - $storeConfigData = array_merge( + return array_merge( $this->getBaseConfigData($store), $this->getExtendedConfigData((int)$store->getId()) ); - return $storeConfigData; } /** @@ -72,7 +71,7 @@ private function getBaseConfigData(StoreInterface $store) : array { $storeConfig = current($this->storeConfigManager->getStoreConfigs([$store->getCode()])); - $storeConfigData = [ + return [ 'id' => $storeConfig->getId(), 'code' => $storeConfig->getCode(), 'website_id' => $storeConfig->getWebsiteId(), @@ -88,9 +87,9 @@ private function getBaseConfigData(StoreInterface $store) : array 'secure_base_url' => $storeConfig->getSecureBaseUrl(), 'secure_base_link_url' => $storeConfig->getSecureBaseLinkUrl(), 'secure_base_static_url' => $storeConfig->getSecureBaseStaticUrl(), - 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl() + 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl(), + 'store_name' => $store->getName() ]; - return $storeConfigData; } /** diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml index f3771b704c3e9..3a0143821d8b9 100644 --- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml @@ -23,11 +23,4 @@ </argument> </arguments> </type> - <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> - <arguments> - <argument name="extendedConfigData" xsi:type="array"> - <item name="store_name" xsi:type="string">store/information/name</item> - </argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/Theme/Block/Html/Pager.php b/app/code/Magento/Theme/Block/Html/Pager.php index 5798b94e31a70..764b2e9ca42f0 100644 --- a/app/code/Magento/Theme/Block/Html/Pager.php +++ b/app/code/Magento/Theme/Block/Html/Pager.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Theme\Block\Html; /** @@ -466,7 +467,26 @@ public function getPageUrl($page) */ public function getLimitUrl($limit) { - return $this->getPagerUrl([$this->getLimitVarName() => $limit]); + return $this->getPagerUrl($this->getPageLimitParams($limit)); + } + + /** + * Return page limit params + * + * @param int $limit + * @return array + */ + private function getPageLimitParams(int $limit): array + { + $data = [$this->getLimitVarName() => $limit]; + + $currentPage = $this->getCurrentPage(); + $availableCount = (int) ceil($this->getTotalNum() / $limit); + if ($currentPage !== 1 && $availableCount < $currentPage) { + $data = array_merge($data, [$this->getPageVarName() => $availableCount === 1 ? null : $availableCount]); + } + + return $data; } /** diff --git a/app/code/Magento/Theme/Model/Config/Customization.php b/app/code/Magento/Theme/Model/Config/Customization.php index 6a6872d794b1b..7430730451110 100644 --- a/app/code/Magento/Theme/Model/Config/Customization.php +++ b/app/code/Magento/Theme/Model/Config/Customization.php @@ -5,23 +5,34 @@ */ namespace Magento\Theme\Model\Config; +use Magento\Framework\App\Area; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; +use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver; + /** * Theme customization config model */ class Customization { /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\View\DesignInterface + * @var DesignInterface */ protected $_design; /** - * @var \Magento\Framework\View\Design\Theme\ThemeProviderInterface + * @var ThemeProviderInterface */ protected $themeProvider; @@ -40,20 +51,28 @@ class Customization * @see self::_prepareThemeCustomizations() */ protected $_unassignedTheme; + /** + * @var StoreUserAgentThemeResolver|mixed|null + */ + private $storeThemesResolver; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\View\DesignInterface $design - * @param \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider + * @param StoreManagerInterface $storeManager + * @param DesignInterface $design + * @param ThemeProviderInterface $themeProvider + * @param StoreThemesResolverInterface|null $storeThemesResolver */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\View\DesignInterface $design, - \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider + StoreManagerInterface $storeManager, + DesignInterface $design, + ThemeProviderInterface $themeProvider, + ?StoreThemesResolverInterface $storeThemesResolver = null ) { $this->_storeManager = $storeManager; $this->_design = $design; $this->themeProvider = $themeProvider; + $this->storeThemesResolver = $storeThemesResolver + ?? ObjectManager::getInstance()->get(StoreThemesResolverInterface::class); } /** @@ -93,13 +112,14 @@ public function getStoresByThemes() { $storesByThemes = []; $stores = $this->_storeManager->getStores(); - /** @var $store \Magento\Store\Model\Store */ + /** @var $store Store */ foreach ($stores as $store) { - $themeId = $this->_getConfigurationThemeId($store); - if (!isset($storesByThemes[$themeId])) { - $storesByThemes[$themeId] = []; + foreach ($this->storeThemesResolver->getThemes($store) as $themeId) { + if (!isset($storesByThemes[$themeId])) { + $storesByThemes[$themeId] = []; + } + $storesByThemes[$themeId][] = $store; } - $storesByThemes[$themeId][] = $store; } return $storesByThemes; } @@ -107,8 +127,8 @@ public function getStoresByThemes() /** * Check if current theme has assigned to any store * - * @param \Magento\Framework\View\Design\ThemeInterface $theme - * @param null|\Magento\Store\Model\Store $store + * @param ThemeInterface $theme + * @param null|Store $store * @return bool */ public function isThemeAssignedToStore($theme, $store = null) @@ -133,8 +153,8 @@ public function hasThemeAssigned() /** * Is theme assigned to specific store * - * @param \Magento\Framework\View\Design\ThemeInterface $theme - * @param \Magento\Store\Model\Store $store + * @param ThemeInterface $theme + * @param Store $store * @return bool */ protected function _isThemeAssignedToSpecificStore($theme, $store) @@ -145,21 +165,21 @@ protected function _isThemeAssignedToSpecificStore($theme, $store) /** * Get configuration theme id * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return int */ protected function _getConfigurationThemeId($store) { return $this->_design->getConfigurationDesignTheme( - \Magento\Framework\App\Area::AREA_FRONTEND, + Area::AREA_FRONTEND, ['store' => $store] ); } /** * Fetch theme customization and sort them out to arrays: - * self::_assignedTheme and self::_unassignedTheme. * + * Set self::_assignedTheme and self::_unassignedTheme. * NOTE: To get into "assigned" list theme customization not necessary should be assigned to store-view directly. * It can be set to website or as default theme and be used by store-view via config fallback mechanism. * @@ -167,15 +187,15 @@ protected function _getConfigurationThemeId($store) */ protected function _prepareThemeCustomizations() { - /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection */ - $themeCollection = $this->themeProvider->getThemeCustomizations(\Magento\Framework\App\Area::AREA_FRONTEND); + /** @var Collection $themeCollection */ + $themeCollection = $this->themeProvider->getThemeCustomizations(Area::AREA_FRONTEND); $assignedThemes = $this->getStoresByThemes(); $this->_assignedTheme = []; $this->_unassignedTheme = []; - /** @var $theme \Magento\Framework\View\Design\ThemeInterface */ + /** @var $theme ThemeInterface */ foreach ($themeCollection as $theme) { if (isset($assignedThemes[$theme->getId()])) { $theme->setAssignedStores($assignedThemes[$theme->getId()]); diff --git a/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php new file mode 100644 index 0000000000000..26bd5604294d1 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Framework\App\Area; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store default theme resolver. + * + * Use system config fallback mechanism if no theme is directly assigned to the store-view. + */ +class StoreDefaultThemeResolver implements StoreThemesResolverInterface +{ + /** + * @var CollectionFactory + */ + private $themeCollectionFactory; + /** + * @var DesignInterface + */ + private $design; + /** + * @var ThemeInterface[] + */ + private $registeredThemes; + + /** + * @param CollectionFactory $themeCollectionFactory + * @param DesignInterface $design + */ + public function __construct( + CollectionFactory $themeCollectionFactory, + DesignInterface $design + ) { + $this->design = $design; + $this->themeCollectionFactory = $themeCollectionFactory; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $theme = $this->design->getConfigurationDesignTheme( + Area::AREA_FRONTEND, + ['store' => $store] + ); + $themes = []; + if ($theme) { + if (!is_numeric($theme)) { + $registeredThemes = $this->getRegisteredThemes(); + if (isset($registeredThemes[$theme])) { + $themes[] = $registeredThemes[$theme]->getId(); + } + } else { + $themes[] = $theme; + } + } + return $themes; + } + + /** + * Get system registered themes. + * + * @return ThemeInterface[] + */ + private function getRegisteredThemes(): array + { + if ($this->registeredThemes === null) { + $this->registeredThemes = []; + /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $collection */ + $collection = $this->themeCollectionFactory->create(); + $themes = $collection->loadRegisteredThemes(); + /** @var ThemeInterface $theme */ + foreach ($themes as $theme) { + $this->registeredThemes[$theme->getCode()] = $theme; + } + } + return $this->registeredThemes; + } +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php new file mode 100644 index 0000000000000..5be86c08f7c51 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use InvalidArgumentException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store associated themes resolver. + */ +class StoreThemesResolver implements StoreThemesResolverInterface +{ + /** + * @var StoreThemesResolverInterface[] + */ + private $resolvers; + + /** + * @param StoreThemesResolverInterface[] $resolvers + */ + public function __construct( + array $resolvers + ) { + foreach ($resolvers as $resolver) { + if (!$resolver instanceof StoreThemesResolverInterface) { + throw new InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + StoreThemesResolverInterface::class, + get_class($resolver) + ) + ); + } + } + $this->resolvers = $resolvers; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $themes = []; + foreach ($this->resolvers as $resolver) { + foreach ($resolver->getThemes($store) as $theme) { + $themes[] = $theme; + } + } + return array_values(array_unique($themes)); + } +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php new file mode 100644 index 0000000000000..bb2cd73300c02 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Store\Api\Data\StoreInterface; + +/** + * Store associated themes resolver. + */ +interface StoreThemesResolverInterface +{ + /** + * Get themes associated with a store view + * + * @param StoreInterface $store + * @return int[] + */ + public function getThemes(StoreInterface $store): array; +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php new file mode 100644 index 0000000000000..fb5d68e37c99b --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store associated themes in user-agent rules resolver, + */ +class StoreUserAgentThemeResolver implements StoreThemesResolverInterface +{ + private const XML_PATH_THEME_USER_AGENT = 'design/theme/ua_regexp'; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** + * @var Json + */ + private $serializer; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param Json $serializer + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Json $serializer + ) { + $this->scopeConfig = $scopeConfig; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $config = $this->scopeConfig->getValue( + self::XML_PATH_THEME_USER_AGENT, + ScopeInterface::SCOPE_STORE, + $store + ); + $rules = $config ? $this->serializer->unserialize($config) : []; + $themes = []; + if ($rules) { + $themes = array_values(array_unique(array_column($rules, 'value'))); + } + return $themes; + } +} diff --git a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php similarity index 88% rename from lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php rename to app/code/Magento/Theme/Plugin/LoadDesignPlugin.php index 2cda49c43c2ce..c4f8d3a905d0f 100644 --- a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php +++ b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -namespace Magento\Framework\App\Action\Plugin; +namespace Magento\Theme\Plugin; use Magento\Framework\App\ActionInterface; use Magento\Framework\Config\Dom\ValidationException; @@ -21,12 +21,12 @@ class LoadDesignPlugin /** * @var DesignLoader */ - protected $_designLoader; + private $designLoader; /** * @var MessageManagerInterface */ - protected $messageManager; + private $messageManager; /** * @param DesignLoader $designLoader @@ -36,7 +36,7 @@ public function __construct( DesignLoader $designLoader, MessageManagerInterface $messageManager ) { - $this->_designLoader = $designLoader; + $this->designLoader = $designLoader; $this->messageManager = $messageManager; } @@ -50,7 +50,7 @@ public function __construct( public function beforeExecute(ActionInterface $subject) { try { - $this->_designLoader->load(); + $this->designLoader->load(); } catch (LocalizedException $e) { if ($e->getPrevious() instanceof ValidationException) { /** @var MessageInterface $message */ diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php index ac16c56b17f1b..fd0ef1db0219a 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php @@ -91,6 +91,60 @@ public function testGetPages(): void $this->assertEquals($expectedPages, $this->pager->getPages()); } + /** + * Test get limit url. + * + * @dataProvider limitUrlDataProvider + * + * @param int $page + * @param int $size + * @param int $limit + * @param array $expectedParams + * @return void + */ + public function testGetLimitUrl(int $page, int $size, int $limit, array $expectedParams): void + { + $expectedArray = [ + '_current' => true, + '_escape' => true, + '_use_rewrite' => true, + '_fragment' => null, + '_query' => $expectedParams, + ]; + + $collectionMock = $this->createMock(Collection::class); + $collectionMock->expects($this->once()) + ->method('getCurPage') + ->willReturn($page); + $collectionMock->expects($this->once()) + ->method('getSize') + ->willReturn($size); + $this->setCollectionProperty($collectionMock); + + $this->urlBuilderMock->expects($this->once()) + ->method('getUrl') + ->with('*/*/*', $expectedArray); + + $this->pager->getLimitUrl($limit); + } + + /** + * DataProvider for testGetLimitUrl + * + * @return array + */ + public function limitUrlDataProvider(): array + { + return [ + [2, 21, 10, ['limit' => 10]], + [3, 21, 10, ['limit' => 10]], + [2, 21, 20, ['limit' => 20]], + [3, 21, 50, ['limit' => 50, 'p' => null]], + [2, 11, 20, ['limit' => 20, 'p' => null]], + [4, 40, 20, ['limit' => 20, 'p' => 2]], + ]; + } + /** * Set Collection * diff --git a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php index 82678d4b4277d..438853b9935e6 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php @@ -13,9 +13,10 @@ use Magento\Framework\App\Area; use Magento\Framework\DataObject; use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Theme\Model\Config\Customization; -use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; use Magento\Theme\Model\Theme\ThemeProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -32,47 +33,37 @@ class CustomizationTest extends TestCase */ protected $designPackage; - /** - * @var Collection - */ - protected $themeCollection; - /** * @var Customization */ protected $model; /** - * @var ThemeProvider|\PHPUnit\Framework\MockObject_MockBuilder + * @var ThemeProvider|MockObject */ protected $themeProviderMock; + /** + * @var StoreThemesResolverInterface|MockObject + */ + private $storeThemesResolver; protected function setUp(): void { - $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) - ->getMock(); - $this->designPackage = $this->getMockBuilder(DesignInterface::class) - ->getMock(); - $this->themeCollection = $this->getMockBuilder(Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $collectionFactory = $this->getMockBuilder(\Magento\Theme\Model\ResourceModel\Theme\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $collectionFactory->expects($this->any())->method('create')->willReturn($this->themeCollection); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); + $this->designPackage = $this->getMockBuilder(DesignInterface::class)->getMock(); $this->themeProviderMock = $this->getMockBuilder(ThemeProvider::class) ->disableOriginalConstructor() ->setMethods(['getThemeCustomizations', 'getThemeByFullPath']) ->getMock(); + $this->storeThemesResolver = $this->createMock(StoreThemesResolverInterface::class); + $this->model = new Customization( $this->storeManager, $this->designPackage, - $this->themeProviderMock + $this->themeProviderMock, + $this->storeThemesResolver ); } @@ -84,13 +75,15 @@ protected function setUp(): void */ public function testGetAssignedThemeCustomizations() { - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); - + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); + + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -108,13 +101,15 @@ public function testGetAssignedThemeCustomizations() */ public function testGetUnassignedThemeCustomizations() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -131,13 +126,15 @@ public function testGetUnassignedThemeCustomizations() */ public function testGetStoresByThemes() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $stores = $this->model->getStoresByThemes(); $this->assertArrayHasKey($this->getAssignedTheme()->getId(), $stores); @@ -148,15 +145,17 @@ public function testGetStoresByThemes() * @covers \Magento\Theme\Model\Config\Customization::_getConfigurationThemeId * @covers \Magento\Theme\Model\Config\Customization::__construct */ - public function testIsThemeAssignedToDefaultStore() + public function testIsThemeAssignedToAnyStore() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -198,10 +197,10 @@ protected function getUnassignedTheme() } /** - * @return DataObject + * @return StoreInterface|MockObject */ protected function getStore() { - return new DataObject(['id' => 55]); + return $this->createConfiguredMock(StoreInterface::class, ['getId' => 55]); } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php new file mode 100644 index 0000000000000..939b47a42ce85 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use ArrayIterator; +use Magento\Framework\App\Area; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; +use Magento\Theme\Model\Theme\StoreDefaultThemeResolver; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store default theme resolver. + */ +class StoreDefaultThemeResolverTest extends TestCase +{ + /** + * @var DesignInterface|MockObject + */ + private $design; + /** + * @var StoreDefaultThemeResolver + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $themeCollectionFactory = $this->createMock(CollectionFactory::class); + $this->design = $this->createMock(DesignInterface::class); + $this->model = new StoreDefaultThemeResolver( + $themeCollectionFactory, + $this->design + ); + $registeredThemes = []; + $registeredThemes[] = $this->createConfiguredMock( + ThemeInterface::class, + [ + 'getId' => 1, + 'getCode' => 'Magento/luma', + ] + ); + $registeredThemes[] = $this->createConfiguredMock( + ThemeInterface::class, + [ + 'getId' => 2, + 'getCode' => 'Magento/blank', + ] + ); + $collection = $this->createMock(Collection::class); + $collection->method('getIterator') + ->willReturn(new ArrayIterator($registeredThemes)); + $collection->method('loadRegisteredThemes') + ->willReturnSelf(); + $themeCollectionFactory->method('create') + ->willReturn($collection); + } + + /** + * Test that method returns default theme associated to given store. + * + * @param string|null $defaultTheme + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(?string $defaultTheme, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + $this->design->expects($this->once()) + ->method('getConfigurationDesignTheme') + ->with( + Area::AREA_FRONTEND, + ['store' => $store] + ) + ->willReturn($defaultTheme); + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + null, + [] + ], + [ + '1', + [1] + ], + [ + 'Magento/blank', + [2] + ], + [ + 'Magento/theme', + [] + ] + ]; + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php new file mode 100644 index 0000000000000..b80ec4ae83887 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\Theme\StoreThemesResolver; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store composite themes resolver model. + */ +class StoreThemesResolverTest extends TestCase +{ + /** + * @var StoreThemesResolverInterface[]|MockObject[] + */ + private $resolvers; + /** + * @var StoreThemesResolver + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->resolvers = []; + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->model = new StoreThemesResolver($this->resolvers); + } + + /** + * Test that constructor SHOULD throw an exception when resolver is not instance of StoreThemesResolverInterface. + */ + public function testInvalidConstructorArguments(): void + { + $resolver = $this->createMock(StoreInterface::class); + $this->expectExceptionObject( + new \InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + StoreThemesResolverInterface::class, + get_class($resolver) + ) + ) + ); + $this->model = new StoreThemesResolver( + [ + $resolver + ] + ); + } + + /** + * Test that method returns aggregated themes from resolvers + * + * @param array $themes + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(array $themes, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + foreach ($this->resolvers as $key => $resolver) { + $resolver->expects($this->once()) + ->method('getThemes') + ->willReturn($themes[$key]); + } + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + [ + [], + [], + [] + ], + [] + ], + [ + [ + ['1'], + [], + ['1'] + ], + ['1'] + ], + [ + [ + ['1'], + ['2'], + ['1'] + ], + ['1', '2'] + ] + ]; + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php new file mode 100644 index 0000000000000..1ef4b17ca6562 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store associated themes in user-agent rules resolver. + */ +class StoreUserAgentThemeResolverTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + /** + * @var Json + */ + private $serializer; + /** + * @var StoreUserAgentThemeResolver + */ + private $model; + + protected function setUp(): void + { + parent::setUp(); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->serializer = new Json(); + $this->model = new StoreUserAgentThemeResolver( + $this->scopeConfig, + $this->serializer + ); + } + + /** + * Test that method returns user-agent rules associated themes. + * + * @param array|null $config + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(?array $config, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->with('design/theme/ua_regexp', ScopeInterface::SCOPE_STORE, $store) + ->willReturn($config !== null ? $this->serializer->serialize($config) : $config); + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + null, + [] + ], + [ + [], + [] + ], + [ + [ + [ + 'search' => '\/Chrome\/i', + 'regexp' => '\/Chrome\/i', + 'value' => '1', + ], + ], + ['1'] + ], + [ + [ + [ + 'search' => '\/Chrome\/i', + 'regexp' => '\/Chrome\/i', + 'value' => '1', + ], + [ + 'search' => '\/mozila\/i', + 'regexp' => '\/mozila\/i', + 'value' => '2', + ], + ], + ['1', '2'] + ] + ]; + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php similarity index 80% rename from lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php rename to app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php index 549d45a986cf0..4efcc584986d1 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php +++ b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php @@ -3,15 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - -namespace Magento\Framework\App\Test\Unit\Action\Plugin; +namespace Magento\Theme\Test\Unit\Plugin; use Magento\Framework\App\Action\Action; -use Magento\Framework\App\Action\Plugin\LoadDesignPlugin; use Magento\Framework\App\ActionInterface; use Magento\Framework\Message\ManagerInterface; use Magento\Framework\View\DesignLoader; +use Magento\Theme\Plugin\LoadDesignPlugin; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,7 +24,7 @@ public function testBeforeExecute() $designLoaderMock = $this->createMock(DesignLoader::class); /** @var MockObject|ManagerInterface $messageManagerMock */ - $messageManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $messageManagerMock = $this->createMock(ManagerInterface::class); $plugin = new LoadDesignPlugin($designLoaderMock, $messageManagerMock); diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml index 921e6bfc6ecf1..15107adb931c9 100644 --- a/app/code/Magento/Theme/etc/di.xml +++ b/app/code/Magento/Theme/etc/di.xml @@ -18,6 +18,7 @@ <preference for="Magento\Theme\Api\DesignConfigRepositoryInterface" type="Magento\Theme\Model\DesignConfigRepository"/> <preference for="Magento\Framework\View\Model\PageLayout\Config\BuilderInterface" type="Magento\Theme\Model\PageLayout\Config\Builder"/> <preference for="Magento\Theme\Model\Design\Config\MetadataProviderInterface" type="Magento\Theme\Model\Design\Config\MetadataProvider"/> + <preference for="Magento\Theme\Model\Theme\StoreThemesResolverInterface" type="Magento\Theme\Model\Theme\StoreThemesResolver"/> <type name="Magento\Theme\Model\Config"> <arguments> <argument name="configCache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> @@ -104,6 +105,9 @@ <argument name="scope" xsi:type="const">Magento\Store\Model\ScopeInterface::SCOPE_STORE</argument> </arguments> </virtualType> + <type name="Magento\Framework\App\ActionInterface"> + <plugin name="designLoader" type="Magento\Theme\Plugin\LoadDesignPlugin"/> + </type> <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> <arguments> <argument name="collections" xsi:type="array"> @@ -309,4 +313,12 @@ <argument name="cache" xsi:type="object">configured_design_cache</argument> </arguments> </type> + <type name="Magento\Theme\Model\Theme\StoreThemesResolver"> + <arguments> + <argument name="resolvers" xsi:type="array"> + <item name="storeDefaultTheme" xsi:type="object">Magento\Theme\Model\Theme\StoreDefaultThemeResolver</item> + <item name="storeUserAgentTheme" xsi:type="object">Magento\Theme\Model\Theme\StoreUserAgentThemeResolver</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml index 55798169cdf75..b42cabde6cd85 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml @@ -14,7 +14,6 @@ <script> require([ 'jquery', - 'jquery-ui-modules/datepicker' ], function($){ //<![CDATA[ @@ -34,7 +33,7 @@ require([ timeText: "<?= $block->escapeJs(__('Time')) ?>", hourText: "<?= $block->escapeJs(__('Hour')) ?>", minuteText: "<?= $block->escapeJs(__('Minute')) ?>", - dateFormat: $.datepicker.RFC_2822, + dateFormat: "D, d M yy", // $.datepicker.RFC_2822 showOn: "button", showAnim: "", changeMonth: true, diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js index a3bd16cab718e..2119426a5c157 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js @@ -82,9 +82,9 @@ } } - function attemptMerge(e1, e2, differentStylesMasterElement, mergeParagraphs) { - if (canMerge(e1, e2, !!differentStylesMasterElement, mergeParagraphs)) { - return merge(e1, e2, differentStylesMasterElement); + function attemptMerge(e1, e2, differentStylesMainElement, mergeParagraphs) { + if (canMerge(e1, e2, !!differentStylesMainElement, mergeParagraphs)) { + return merge(e1, e2, differentStylesMainElement); } else if (e1 && e1.tagName === 'LI' && isList(e2)) { // Fix invalidly nested lists. e1.appendChild(e2); @@ -112,7 +112,7 @@ return firstChild && lastChild && firstChild === lastChild && isList(firstChild); } - function merge(e1, e2, masterElement) { + function merge(e1, e2, mainElement) { var lastOriginal = skipWhitespaceNodesBackwards(e1.lastChild), firstNew = skipWhitespaceNodesForwards(e2.firstChild); if (e1.tagName === 'P') { e1.appendChild(e1.ownerDocument.createElement('br')); @@ -120,8 +120,8 @@ while (e2.firstChild) { e1.appendChild(e2.firstChild); } - if (masterElement) { - e1.style.listStyleType = masterElement.style.listStyleType; + if (mainElement) { + e1.style.listStyleType = mainElement.style.listStyleType; } e2.parentNode.removeChild(e2); attemptMerge(lastOriginal, firstNew, false); @@ -164,7 +164,7 @@ } return false; } - + // If we are at the end of a paragraph in a list item, pressing enter should create a new list item instead of a new paragraph. function isEndOfParagraph() { var node = ed.selection.getNode(); @@ -241,7 +241,7 @@ Event.cancel(e); } } - + // Creates a new list item after the current selection's list item parent function createNewLi(ed, e) { if (state == LIST_PARAGRAPH) { diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js index 2fab8c219c02a..284d395d8120b 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js @@ -7,11 +7,8 @@ define([ 'ko', 'underscore', 'jquery', - 'mage/translate', - 'mage/calendar', - 'moment', - 'mageUtils' -], function (ko, _, $, $t, calendar, moment, utils) { + 'mage/translate' +], function (ko, _, $, $t) { 'use strict'; var defaults = { @@ -46,10 +43,12 @@ define([ observable = config; } - $(el).calendar(options); + require(['mage/calendar'], function () { + $(el).calendar(options); - ko.utils.registerEventHandler(el, 'change', function () { - observable(this.value); + ko.utils.registerEventHandler(el, 'change', function () { + observable(this.value); + }); }); }, @@ -62,6 +61,7 @@ define([ */ update: function (element, valueAccessor) { var config = valueAccessor(), + $element = $(element), observable, options = {}, newVal; @@ -75,26 +75,21 @@ define([ observable = config; } - if (_.isEmpty(observable())) { - if ($(element).datepicker('getDate')) { - $(element).datepicker('setDate', null); - $(element).blur(); + require(['moment', 'mage/utils/misc', 'mage/calendar'], function (moment, utils) { + if (_.isEmpty(observable())) { + newVal = null; + } else { + newVal = moment( + observable(), + utils.convertToMomentFormat( + options.dateFormat + (options.showsTime ? ' ' + options.timeFormat : '') + ) + ).toDate(); } - } else { - newVal = moment( - observable(), - utils.convertToMomentFormat( - options.dateFormat + (options.showsTime ? ' ' + options.timeFormat : '') - ) - ).toDate(); - if ($(element).datepicker('getDate') == null || - newVal.valueOf() !== $(element).datepicker('getDate').valueOf() - ) { - $(element).datepicker('setDate', newVal); - $(element).blur(); - } - } + $element.datepicker('setDate', newVal); + $element.blur(); + }); } }; }); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js index 1dda3254f4613..52031dc0c3792 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js @@ -7,13 +7,18 @@ define([ 'ko', 'jquery', 'underscore', - '../template/renderer', - 'jquery-ui-modules/slider' + '../template/renderer' ], function (ko, $, _, renderer) { 'use strict'; var isTouchDevice = !_.isUndefined(document.ontouchstart), - sliderFn = 'slider'; + sliderFn = 'slider', + sliderModule = 'jquery-ui-modules/slider'; + + if (isTouchDevice) { + sliderFn = 'touchSlider'; + sliderModule = 'mage/touch-slider'; + } ko.bindingHandlers.range = { @@ -41,7 +46,9 @@ define([ } }); - $(element)[sliderFn](config); + require([sliderModule], function () { + $(element)[sliderFn](config); + }); }, /** @@ -55,149 +62,11 @@ define([ config.value = ko.unwrap(config.value); - $(element)[sliderFn]('option', config); + require([sliderModule], function () { + $(element)[sliderFn]('option', config); + }); } }; renderer.addAttribute('range'); - - if (!isTouchDevice) { - return; - } - - $.widget('mage.touchSlider', $.ui.slider, { - - /** - * Creates instance of widget. - * - * @override - */ - _create: function () { - _.bindAll( - this, - '_mouseDown', - '_mouseMove', - '_onTouchEnd' - ); - - return this._superApply(arguments); - }, - - /** - * Initializes mouse events on element. - * @override - */ - _mouseInit: function () { - var result = this._superApply(arguments); - - this.element - .off('mousedown.' + this.widgetName) - .on('touchstart.' + this.widgetName, this._mouseDown); - - return result; - }, - - /** - * Elements' 'mousedown' event handler polyfill. - * @override - */ - _mouseDown: function (event) { - var prevDelegate = this._mouseMoveDelegate, - result; - - event = this._touchToMouse(event); - result = this._super(event); - - if (prevDelegate === this._mouseMoveDelegate) { - return result; - } - - $(document) - .off('mousemove.' + this.widgetName) - .off('mouseup.' + this.widgetName); - - $(document) - .on('touchmove.' + this.widgetName, this._mouseMove) - .on('touchend.' + this.widgetName, this._onTouchEnd) - .on('tochleave.' + this.widgetName, this._onTouchEnd); - - return result; - }, - - /** - * Documents' 'mousemove' event handler polyfill. - * - * @override - * @param {Event} event - Touch event object. - */ - _mouseMove: function (event) { - event = this._touchToMouse(event); - - return this._super(event); - }, - - /** - * Documents' 'touchend' event handler. - */ - _onTouchEnd: function (event) { - $(document).trigger('mouseup'); - - return this._mouseUp(event); - }, - - /** - * Removes previously assigned touch handlers. - * - * @override - */ - _mouseUp: function () { - this._removeTouchHandlers(); - - return this._superApply(arguments); - }, - - /** - * Removes previously assigned touch handlers. - * - * @override - */ - _mouseDestroy: function () { - this._removeTouchHandlers(); - - return this._superApply(arguments); - }, - - /** - * Removes touch events from document object. - */ - _removeTouchHandlers: function () { - $(document) - .off('touchmove.' + this.widgetName) - .off('touchend.' + this.widgetName) - .off('touchleave.' + this.widgetName); - }, - - /** - * Adds properties to the touch event to mimic mouse event. - * - * @param {Event} event - Touch event object. - * @returns {Event} - */ - _touchToMouse: function (event) { - var orig = event.originalEvent, - touch = orig.touches[0]; - - return _.extend(event, { - which: 1, - pageX: touch.pageX, - pageY: touch.pageY, - clientX: touch.clientX, - clientY: touch.clientY, - screenX: touch.screenX, - screenY: touch.screenY - }); - } - }); - - sliderFn = 'touchSlider'; }); diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..a409860811837 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminTargetPathInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the target path is shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="targetPath" type="string"/> + </arguments> + + <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', targetPath)}}" + stepKey="seeValueInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..739675ba264ea --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the target path is not shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="targetPath" type="string"/> + </arguments> + + <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', targetPath)}}" + stepKey="valueIsNotShownInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml index 4e46ed8e4fc79..3b140aed5f572 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml @@ -47,84 +47,103 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersIfSet"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> + <!--Change category name and URL key for EN Store View--> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewEn"> <argument name="Store" value="customStoreENNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForENStoreView"> + <argument name="categoryName" value="categoryEN"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyENStoreView"> <argument name="value" value="category-english"/> </actionGroup> + + <!--Change category name and URL key for NL Store View--> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewNl"> <argument name="Store" value="customStoreNLNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForNLStoreView"> + <argument name="categoryName" value="categoryNL"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyNLStoreView"> <argument name="value" value="category-dutch"/> </actionGroup> - <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> - <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> - <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> - <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> - <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> - <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> - <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> - <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> - <argument name="productName" value="productformagetwo68980"/> - </actionGroup> - <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo68980')}}" stepKey="clickOnProductRow"/> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProduct"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="import_updated.csv"/> + <argument name="importNoticeMessage" value="Created: 1, Updated: 0, Deleted: 0"/> + <argument name="validationNoticeMessage" value="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="productformagetwo68980"/> + </actionGroup> <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + <!--Open Marketing - SEO & Search - URL Rewrites--> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters2"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlRewriteForENStoreView"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters3"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView1"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters3"/> - <waitForPageLoad stepKey="waitForPageToLoad3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlRewriteForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters4"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english/productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView2"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters4"/> - <waitForPageLoad stepKey="waitForPageToLoad4"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english/productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters5"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch/productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView3"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters5"/> - <waitForPageLoad stepKey="waitForPageToLoad5"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch/productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml index 1d604ef7648dc..7d5bb78f3b3f9 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml @@ -64,9 +64,9 @@ <argument name="Store" value="customStoreENNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForENStoreView"> + <argument name="categoryName" value="categoryenglish"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyENStoreView"> <argument name="value" value="category-english"/> </actionGroup> @@ -74,82 +74,114 @@ <argument name="Store" value="customStoreNLNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForNLStoreView"> + <argument name="categoryName" value="categorydutch"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyNLStoreView"> <argument name="value" value="category-dutch"/> </actionGroup> - <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> - <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> - <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> - <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> - <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> - <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> - <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> - <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> - <argument name="productName" value="productformagetwo68980"/> - </actionGroup> - <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo68980')}}" stepKey="clickOnProductRow"/> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProduct"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="import_updated.csv"/> + <argument name="importNoticeMessage" value="Created: 1, Updated: 0, Deleted: 0"/> + <argument name="validationNoticeMessage" value="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="productformagetwo68980"/> + </actionGroup> <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + <!-- Open Marketing - SEO & Search - URL Rewrites --> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters2"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english/productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrl"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeRequestPathForProduct"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeTargetPathForProduct"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeRequestPathForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeTargetPathForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters3"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView1"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters3"/> - <waitForPageLoad stepKey="waitForPageToLoad3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch/productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeRequestPathForProductForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeTargetPathForProductForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeRequestPathForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeTargetPathForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> <!-- Switch StoreView --> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnStoreFrontHomePage"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> <argument name="storeView" value="customStoreENNotUnique"/> </actionGroup> - <amOnPage url="/productformagetwo68980-english.html" stepKey="navigateToProductPage"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-english" stepKey="seeProductName"/> - <amOnPage url="/category-english/productformagetwo68980-english.html" stepKey="navigateToProductPage2"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-english" stepKey="seeProductName2"/> + <!-- Assert Redirects work and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="productformagetwo68980-english"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSecondAttempt"> + <argument name="productName" value="productformagetwo68980-english"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/category-english/productformagetwo68980-english.html"/> + </actionGroup> <!-- Switch StoreView --> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page2"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="backToHomePage"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView2"> <argument name="storeView" value="customStoreNLNotUnique"/> </actionGroup> - <amOnPage url="/productformagetwo68980-dutch.html" stepKey="navigateToProductPage3"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-dutch" stepKey="seeProductName3"/> - <amOnPage url="/category-dutch/productformagetwo68980-dutch.html" stepKey="navigateToProductPage4"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-dutch" stepKey="seeProductName4"/> + <!-- Assert Redirects work and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageThirdAttempt"> + <argument name="productName" value="productformagetwo68980-dutch"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageFourthAttempt"> + <argument name="productName" value="productformagetwo68980-dutch"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml index 9d6b267055f70..a1adb918d0e8d 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml @@ -44,34 +44,31 @@ </after> <!-- 1. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGrid"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInGrid"> + <argument name="requestPath" value="$createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> <!-- 2. Set the configuration for Generate "category/product" URL Rewrites to No--> - <amOnPage url="{{CatalogConfigPage.url}}" stepKey="amOnCatalogConfigPage"/> - <conditionalClick selector="{{CatalogSection.seo}}" dependentSelector="{{CatalogSection.CheckIfSeoTabExpand}}" visible="true" stepKey="expandSeoTab" /> - <waitForElementVisible selector="{{CatalogSection.GenerateUrlRewrites}}" stepKey="GenerateUrlRewritesSelect"/> - <selectOption userInput="0" selector="{{CatalogSection.GenerateUrlRewrites}}" stepKey="selectUrlGenerationNo" /> - <waitForElementVisible selector="{{GenerateUrlRewritesConfirm.title}}" stepKey="waitForConfirmModal"/> - <click selector="{{GenerateUrlRewritesConfirm.ok}}" stepKey="confirmSwitchingGenerationOff"/> - <click selector="{{CatalogSection.save}}" stepKey="saveConfig" /> - <waitForPageLoad stepKey="waitForSavingSystemConfiguration"/> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!-- 3. Flush cache--> <magentoCLI command="cache:flush" stepKey="cleanCache"/> <!-- 4. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName2"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue1"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="dontSeeValue2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteAfterDisablingTheConfig"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGridAfterDisablingTheConfig"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="categoryUrlIsNotShownAfterDisablingTheConfig"> + <argument name="requestPath" value="$createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/User/Model/Notificator.php b/app/code/Magento/User/Model/Notificator.php index 3a5522db4c533..3e36cd1387e39 100644 --- a/app/code/Magento/User/Model/Notificator.php +++ b/app/code/Magento/User/Model/Notificator.php @@ -107,6 +107,7 @@ public function sendForgotPassword(UserInterface $user): void $this->sendNotification( 'admin/emails/forgot_email_template', [ + 'username' => $user->getFirstName().' '.$user->getLastName(), 'user' => $user, 'store' => $this->storeManager->getStore( Store::DEFAULT_STORE_ID diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml similarity index 83% rename from app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml rename to app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml index 4049e60e83455..d41ed63678783 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml @@ -5,10 +5,8 @@ * 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"> - <!--Login New User--> - <actionGroup name="LoginNewUser"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginNewUserActionGroup" deprecated="Use AdminLoginActionGroup instead"> <annotations> <description>Goes to the Backend Admin Login page. Fill Username and Password. Click on Sign In.</description> </annotations> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml index 668ae550f1b3d..ba8d6ef433e13 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml @@ -32,7 +32,7 @@ <argument name="user" value="activeAdmin"/> <argument name="role" value="roleDefaultAdministrator"/> </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMasterAdmin"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMainAdmin"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToNewAdmin"> <argument name="username" value="{{activeAdmin.username}}"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml index 23a30246bd999..c26821d5be4b2 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml @@ -20,7 +20,7 @@ <group value="mtf_migrated"/> </annotations> - <actionGroup ref="AdminLoginActionGroup" stepKey="adminMasterLogin"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminMainLogin"/> <actionGroup ref="AdminCreateUserWithRoleAndIsActiveActionGroup" stepKey="createAdminUser"> <argument name="user" value="inactiveAdmin"/> <argument name="role" value="roleDefaultAdministrator"/> @@ -29,7 +29,7 @@ <actionGroup ref="AssertAdminUserIsInGridActionGroup" stepKey="assertAdminIsInGrid"> <argument name="user" value="inactiveAdmin"/> </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="adminMasterLogout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminMainLogout"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminNewLogin"> <argument name="username" value="{{inactiveAdmin.username}}"/> diff --git a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html index dacfa640464a3..42240bff3b8db 100644 --- a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html +++ b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html @@ -4,16 +4,17 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Password Reset Confirmation for %name" name=$user.name}} @--> +<!--@subject {{trans "Password Reset Confirmation for %name" name=$username}} @--> <!--@vars { "var store.frontend_name":"Store Name", "var user.id":"Account Holder Id", "var user.rp_token":"Reset Password Token", "var user.name":"Account Holder Name", -"store url=\"admin\/auth\/resetpassword\/\" _query_id=$user.id _query_token=$user.rp_token":"Reset Password URL" +"store url=\"admin\/auth\/resetpassword\/\" _query_id=$user.id _query_token=$user.rp_token":"Reset Password URL", +"var username":"Account Holder Name" } @--> -{{trans "%name," name=$user.name}} +{{trans "%name," name=$username}} {{trans "There was recently a request to change the password for your account."}} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php new file mode 100644 index 0000000000000..7acfb503a5ad0 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Adding products to wishlist + */ +class AddProductsToWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var BuyRequestBuilder + */ + private $buyRequestBuilder; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @param ProductRepositoryInterface $productRepository + * @param BuyRequestBuilder $buyRequestBuilder + * @param WishlistResourceModel $wishlistResource + */ + public function __construct( + ProductRepositoryInterface $productRepository, + BuyRequestBuilder $buyRequestBuilder, + WishlistResourceModel $wishlistResource + ) { + $this->productRepository = $productRepository; + $this->buyRequestBuilder = $buyRequestBuilder; + $this->wishlistResource = $wishlistResource; + } + + /** + * Adding products to wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItems + * + * @return WishlistOutput + * + * @throws AlreadyExistsException + */ + public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput + { + foreach ($wishlistItems as $wishlistItem) { + $this->addItemToWishlist($wishlist, $wishlistItem); + } + + $wishlistOutput = $this->prepareOutput($wishlist); + + if ($wishlist->isObjectNew() || count($wishlistOutput->getErrors()) !== count($wishlistItems)) { + $this->wishlistResource->save($wishlist); + } + + return $wishlistOutput; + } + + /** + * Add product item to wishlist + * + * @param Wishlist $wishlist + * @param WishlistItem $wishlistItem + * + * @return void + */ + private function addItemToWishlist(Wishlist $wishlist, WishlistItem $wishlistItem): void + { + $sku = $wishlistItem->getParentSku() ?? $wishlistItem->getSku(); + + try { + $product = $this->productRepository->get($sku, false, null, true); + } catch (NoSuchEntityException $e) { + $this->addError( + __('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(), + self::ERROR_PRODUCT_NOT_FOUND + ); + + return; + } + + try { + $options = $this->buyRequestBuilder->build($wishlistItem, (int) $product->getId()); + $result = $wishlist->addNewItem($product, $options); + + if (is_string($result)) { + $this->addError($result); + } + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); + } catch (\Throwable $e) { + $this->addError( + __( + 'Could not add the product with SKU "%sku" to the wishlist:: %message', + ['sku' => $sku, 'message' => $e->getMessage()] + )->render() + ); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php new file mode 100644 index 0000000000000..1cfa316c3cd01 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for bundle product buy requests + */ +class BundleDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'bundle'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItem, ?int $productId): array + { + $bundleOptionsData = []; + + foreach ($wishlistItem->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $optionId, $optionValueId, $optionQuantity] = $optionData; + + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + + return $bundleOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php new file mode 100644 index 0000000000000..1f7ddce345b1c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Building buy request for all product types + */ +class BuyRequestBuilder +{ + /** + * @var BuyRequestDataProviderInterface[] + */ + private $providers; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @param DataObjectFactory $dataObjectFactory + * @param array $providers + */ + public function __construct( + DataObjectFactory $dataObjectFactory, + array $providers = [] + ) { + $this->dataObjectFactory = $dataObjectFactory; + $this->providers = $providers; + } + + /** + * Build product buy request for adding to wishlist + * + * @param WishlistItem $wishlistItemData + * @param int|null $productId + * + * @return DataObject + */ + public function build(WishlistItem $wishlistItemData, ?int $productId = null): DataObject + { + $requestData = [ + [ + 'qty' => $wishlistItemData->getQuantity(), + ] + ]; + + foreach ($this->providers as $provider) { + $requestData[] = $provider->execute($wishlistItemData, $productId); + } + + return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php new file mode 100644 index 0000000000000..fac45d7f86c7c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Build buy request for adding products to wishlist + */ +interface BuyRequestDataProviderInterface +{ + /** + * Provide buy request data from add to wishlist item request + * + * @param WishlistItem $wishlistItemData + * @param int|null $productId + * + * @return array + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array; +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php new file mode 100644 index 0000000000000..e8f5bf0654f64 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.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\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for custom options buy requests + */ +class CustomizableOptionDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'custom-option'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $customizableOptionsData = []; + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $optionId, $optionValue] = $optionData; + + $customizableOptionsData[$optionId][] = $optionValue; + } + + foreach ($wishlistItemData->getEnteredOptions() as $option) { + $optionData = \explode('/', base64_decode($option->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $optionId] = $optionData; + + $customizableOptionsData[$optionId][] = $option->getValue(); + } + + if (empty($customizableOptionsData)) { + return $customizableOptionsData; + } + + $result = ['options' => $this->flattenOptionValues($customizableOptionsData)]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Flatten option values for non-multiselect customizable options + * + * @param array $customizableOptionsData + * + * @return array + */ + private function flattenOptionValues(array $customizableOptionsData): array + { + foreach ($customizableOptionsData as $optionId => $optionValue) { + if (count($optionValue) === 1) { + $customizableOptionsData[$optionId] = $optionValue[0]; + } + } + + return $customizableOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php new file mode 100644 index 0000000000000..1ad1a0b64706a --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for downloadable product buy requests + */ +class DownloadableLinkDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'downloadable'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItem, ?int $productId): array + { + $linksData = []; + + foreach ($wishlistItem->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $linkId] = $optionData; + + $linksData[] = $linkId; + } + + return $linksData ? ['links' => $linksData] : []; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php new file mode 100644 index 0000000000000..01e29bcf80c0b --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for configurable product buy requests + */ +class SuperAttributeDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'configurable'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $configurableData = []; + + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $attributeId, $valueIndex] = $optionData; + + $configurableData[$attributeId] = $valueIndex; + } + + if (empty($configurableData)) { + return $configurableData; + } + + $result = ['super_attribute' => $configurableData]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php new file mode 100644 index 0000000000000..a11f631f1f5aa --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for grouped product buy requests + */ +class SuperGroupDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'grouped'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $groupedData = []; + + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $simpleProductId, $quantity] = $optionData; + + $groupedData[$simpleProductId] = $quantity; + } + + if (empty($groupedData)) { + return $groupedData; + } + + $result = ['super_group' => $groupedData]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Config.php b/app/code/Magento/Wishlist/Model/Wishlist/Config.php new file mode 100644 index 0000000000000..041e9f1ca0a21 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Config.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Provides wishlist configuration + */ +class Config +{ + const XML_PATH_WISHLIST_ACTIVE = 'wishlist/general/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check whether the wishlist is enabled or not + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_WISHLIST_ACTIVE, + ScopeInterface::SCOPE_STORES + ); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php new file mode 100644 index 0000000000000..0d6b2a2302540 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents entered options + */ +class EnteredOption +{ + /** + * @var string + */ + private $id; + + /** + * @var string + */ + private $value; + + /** + * @param string $id + * @param string $value + */ + public function __construct(string $id, string $value) + { + $this->id = $id; + $this->value = $value; + } + + /** + * Get entered option id + * + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * Get entered option value + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php new file mode 100644 index 0000000000000..cb8420169fa8a --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents error item + */ +class Error +{ + /** + * @var string + */ + private $message; + + /** + * @var string + */ + private $code; + + /** + * @param string $message + * @param string $code + */ + public function __construct(string $message, string $code) + { + $this->message = $message; + $this->code = $code; + } + + /** + * Get error message + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Get error code + * + * @return string + */ + public function getCode(): string + { + return $this->code; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php new file mode 100644 index 0000000000000..129a61c0a2a6c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents selected option + */ +class SelectedOption +{ + /** + * @var string + */ + private $id; + + /** + * @param string $id + */ + public function __construct(string $id) + { + $this->id = $id; + } + + /** + * Get selected option id + * + * @return string + */ + public function getId(): string + { + return $this->id; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php new file mode 100644 index 0000000000000..236b7f1eee72d --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents Wishlist Item data + */ +class WishlistItem +{ + /** + * @var float + */ + private $quantity; + + /** + * @var string|null + */ + private $sku; + + /** + * @var string + */ + private $parentSku; + + /** + * @var int|null + */ + private $id; + + /** + * @var string|null + */ + private $description; + + /** + * @var SelectedOption[] + */ + private $selectedOptions; + + /** + * @var EnteredOption[] + */ + private $enteredOptions; + + /** + * @param float $quantity + * @param string|null $sku + * @param string|null $parentSku + * @param int|null $id + * @param string|null $description + * @param array|null $selectedOptions + * @param array|null $enteredOptions + */ + public function __construct( + float $quantity, + string $sku = null, + string $parentSku = null, + int $id = null, + string $description = null, + array $selectedOptions = null, + array $enteredOptions = null + ) { + $this->quantity = $quantity; + $this->sku = $sku; + $this->parentSku = $parentSku; + $this->id = $id; + $this->description = $description; + $this->selectedOptions = $selectedOptions; + $this->enteredOptions = $enteredOptions; + } + + /** + * Get wishlist item id + * + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * Get wishlist item description + * + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * Get sku + * + * @return string|null + */ + public function getSku(): ?string + { + return $this->sku; + } + + /** + * Get quantity + * + * @return float + */ + public function getQuantity(): float + { + return $this->quantity; + } + + /** + * Get parent sku + * + * @return string|null + */ + public function getParentSku(): ?string + { + return $this->parentSku; + } + + /** + * Get selected options + * + * @return SelectedOption[]|null + */ + public function getSelectedOptions(): ?array + { + return $this->selectedOptions; + } + + /** + * Get entered options + * + * @return EnteredOption[]|null + */ + public function getEnteredOptions(): ?array + { + return $this->enteredOptions; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php new file mode 100644 index 0000000000000..153e8451bae31 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +use Magento\Framework\Exception\InputException; + +/** + * Create WishlistItem DTO + */ +class WishlistItemFactory +{ + /** + * Create wishlist item DTO + * + * @param array $data + * + * @return WishlistItem + */ + public function create(array $data): WishlistItem + { + return new WishlistItem( + $data['quantity'], + $data['sku'] ?? null, + $data['parent_sku'] ?? null, + isset($data['wishlist_item_id']) ? (int) $data['wishlist_item_id'] : null, + $data['description'] ?? null, + isset($data['selected_options']) ? $this->createSelectedOptions($data['selected_options']) : [], + isset($data['entered_options']) ? $this->createEnteredOptions($data['entered_options']) : [] + ); + } + + /** + * Create array of Entered Options + * + * @param array $options + * + * @return EnteredOption[] + */ + private function createEnteredOptions(array $options): array + { + return \array_map( + function (array $option) { + if (!isset($option['id'], $option['value'])) { + throw new InputException( + __('Required fields are not present EnteredOption.id, EnteredOption.value') + ); + } + return new EnteredOption($option['id'], $option['value']); + }, + $options + ); + } + + /** + * Create array of Selected Options + * + * @param string[] $options + * + * @return SelectedOption[] + */ + private function createSelectedOptions(array $options): array + { + return \array_map( + function ($option) { + return new SelectedOption($option); + }, + $options + ); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php new file mode 100644 index 0000000000000..fc7db9ec910fb --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +use Magento\Wishlist\Model\Wishlist; + +/** + * DTO represent output for \Magento\WishlistGraphQl\Model\Resolver\AddProductsToWishlistResolver + */ +class WishlistOutput +{ + /** + * @var Wishlist + */ + private $wishlist; + + /** + * @var Error[] + */ + private $errors; + + /** + * @param Wishlist $wishlist + * @param Error[] $errors + */ + public function __construct(Wishlist $wishlist, array $errors) + { + $this->wishlist = $wishlist; + $this->errors = $errors; + } + + /** + * Get Wishlist + * + * @return Wishlist + */ + public function getWishlist(): Wishlist + { + return $this->wishlist; + } + + /** + * Get errors happened during adding products to wishlist + * + * @return Error[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php new file mode 100644 index 0000000000000..d143830064752 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Wishlist\Model\Item as WishlistItem; +use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Remove product items from wishlist + */ +class RemoveProductsFromWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var WishlistItemFactory + */ + private $wishlistItemFactory; + + /** + * @var WishlistItemResource + */ + private $wishlistItemResource; + + /** + * @param WishlistItemFactory $wishlistItemFactory + * @param WishlistItemResource $wishlistItemResource + */ + public function __construct( + WishlistItemFactory $wishlistItemFactory, + WishlistItemResource $wishlistItemResource + ) { + $this->wishlistItemFactory = $wishlistItemFactory; + $this->wishlistItemResource = $wishlistItemResource; + } + + /** + * Removing items from wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItemsIds + * + * @return WishlistOutput + */ + public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOutput + { + foreach ($wishlistItemsIds as $wishlistItemId) { + $this->removeItemFromWishlist((int) $wishlistItemId); + } + + return $this->prepareOutput($wishlist); + } + + /** + * Remove product item from wishlist + * + * @param int $wishlistItemId + * + * @return void + */ + private function removeItemFromWishlist(int $wishlistItemId): void + { + try { + /** @var WishlistItem $wishlistItem */ + $wishlistItem = $this->wishlistItemFactory->create(); + $this->wishlistItemResource->load($wishlistItem, $wishlistItemId); + if (!$wishlistItem->getId()) { + $this->addError( + __('Could not find a wishlist item with ID "%id"', ['id' => $wishlistItemId])->render(), + self::ERROR_PRODUCT_NOT_FOUND + ); + } + + $this->wishlistItemResource->delete($wishlistItem); + } catch (\Exception $e) { + $this->addError( + __( + 'We can\'t delete the item with ID "%id" from the Wish List right now.', + ['id' => $wishlistItemId] + )->render() + ); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php new file mode 100644 index 0000000000000..4abcada138362 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Wishlist\Model\Item as WishlistItem; +use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem as WishlistItemData; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Updating product items in wishlist + */ +class UpdateProductsInWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var BuyRequestBuilder + */ + private $buyRequestBuilder; + + /** + * @var WishlistItemFactory + */ + private $wishlistItemFactory; + + /** + * @var WishlistItemResource + */ + private $wishlistItemResource; + + /** + * @param BuyRequestBuilder $buyRequestBuilder + * @param WishlistItemFactory $wishlistItemFactory + * @param WishlistItemResource $wishlistItemResource + */ + public function __construct( + BuyRequestBuilder $buyRequestBuilder, + WishlistItemFactory $wishlistItemFactory, + WishlistItemResource $wishlistItemResource + ) { + $this->buyRequestBuilder = $buyRequestBuilder; + $this->wishlistItemFactory = $wishlistItemFactory; + $this->wishlistItemResource = $wishlistItemResource; + } + + /** + * Adding products to wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItems + * + * @return WishlistOutput + */ + public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput + { + foreach ($wishlistItems as $wishlistItem) { + $this->updateItemInWishlist($wishlist, $wishlistItem); + } + + return $this->prepareOutput($wishlist); + } + + /** + * Update product item in wishlist + * + * @param Wishlist $wishlist + * @param WishlistItemData $wishlistItemData + * + * @return void + */ + private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wishlistItemData): void + { + try { + $options = $this->buyRequestBuilder->build($wishlistItemData); + /** @var WishlistItem $wishlistItem */ + $wishlistItem = $this->wishlistItemFactory->create(); + $this->wishlistItemResource->load($wishlistItem, $wishlistItemData->getId()); + $wishlistItem->setDescription($wishlistItemData->getDescription()); + $resultItem = $wishlist->updateItem($wishlistItem, $options); + + if (is_string($resultItem)) { + $this->addError($resultItem); + } + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml new file mode 100644 index 0000000000000..af229b3507077 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.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="AdminDeleteCustomerWishlistItemTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Wishlist items deleting"/> + <title value="Admin deletes an item from customer wishlist"/> + <description value="Admin Should be able delete items from customer wishlist"/> + <testCaseId value="MC-35170"/> + <group value="wishlist"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$createCategory$"/> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="navigateToCustomerEditPage"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="AdminNavigateCustomerWishlistTabActionGroup" stepKey="navigateToWishlistTab"/> + <actionGroup ref="AdminCustomerFindWishlistItemActionGroup" stepKey="findWishlistItem"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AdminCustomerDeleteWishlistItemActionGroup" stepKey="deleteItem"/> + <actionGroup ref="AssertAdminCustomerNoItemsInWishlistActionGroup" stepKey="assertNoItems"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginOnStoreFront"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="NavigateThroughCustomerTabsActionGroup" stepKey="navigateToWishlist"> + <argument name="navigationItemName" value="My Wish List"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerWishlistIsEmptyActionGroup" stepKey="assertNoItemsInWishlist"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.xml new file mode 100644 index 0000000000000..17d3ff1009b9b --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.xml @@ -0,0 +1,51 @@ +<?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="StorefrontDisabledCustomerWishlistFunctionalityTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Disabled Wishlist Functionality"/> + <title value="Wishlist Functionality is disabled in system configurations and not visible on FE"/> + <description value="Customer should not see wishlist functionality if it's disabled"/> + <testCaseId value="MC-35200"/> + <group value="wishlist"/> + <group value="configuration"/> + </annotations> + <before> + <magentoCLI command="config:set wishlist/general/active 0" stepKey="disableWishlist"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <magentoCLI command="config:set wishlist/general/active 1" stepKey="enableWishlist"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup" stepKey="assertItemIsNotPresent"> + <argument name="itemName" value="My Wish List"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup" stepKey="assertButtonIsAbsent"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml new file mode 100644 index 0000000000000..7ec06e3f3cf4d --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Sharing wishlist with more than Maximum Allowed Emails qty"/> + <title value="Sharing wishlist with more than Maximum Allowed Emails qty"/> + <description value="Customer should not have a possibility share wishlist with more than maximum allowed emails qty"/> + <testCaseId value="MC-35167"/> + <group value="wishlist"/> + <group value="configuration"/> + </annotations> + <before> + <magentoCLI command="config:set wishlist/email/number_limit 1" stepKey="changeEmailsQtyLimit"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <magentoCLI command="config:set wishlist/email/number_limit 10" stepKey="returnDefaultValue"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishList"> + <argument name="email" value="{{Wishlist.shareInfo_emails}}"/> + <argument name="message" value="{{Wishlist.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertMessage"> + <argument name="message" value="Maximum of 1 emails can be sent."/> + <argument name="messageType" value="error"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml index 20881fa64f8f8..0438a1b58e771 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml @@ -14,6 +14,7 @@ <stories value="Customer Wishlist"/> <title value="Customer is not able to share wishlist with invalid email addresses"/> <description value="Customer is not able to share wishlist with invalid email addresses"/> + <severity value="AVERAGE"/> <group value="wishlist"/> </annotations> <before> diff --git a/app/code/Magento/Wishlist/etc/graphql/di.xml b/app/code/Magento/Wishlist/etc/graphql/di.xml new file mode 100644 index 0000000000000..9726376bf30be --- /dev/null +++ b/app/code/Magento/Wishlist/etc/graphql/di.xml @@ -0,0 +1,20 @@ +<?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:ObjectManager/etc/config.xsd"> + <type name="Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="super_attribute" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\SuperAttributeDataProvider</item> + <item name="customizable_option" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\CustomizableOptionDataProvider</item> + <item name="bundle" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\BundleDataProvider</item> + <item name="downloadable" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\DownloadableLinkDataProvider</item> + <item name="grouped" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\SuperGroupDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php new file mode 100644 index 0000000000000..9cc1404613e41 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Mapper; + +use Magento\Wishlist\Model\Wishlist; + +/** + * Prepares the wishlist output as associative array + */ +class WishlistDataMapper +{ + /** + * Mapping the review data + * + * @param Wishlist $wishlist + * + * @return array + */ + public function map(Wishlist $wishlist): array + { + return [ + 'id' => $wishlist->getId(), + 'sharing_code' => $wishlist->getSharingCode(), + 'updated_at' => $wishlist->getUpdatedAt(), + 'items_count' => $wishlist->getItemsCount(), + 'name' => $wishlist->getName(), + 'model' => $wishlist, + ]; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php new file mode 100644 index 0000000000000..11c8446a72a9d --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\AddProductsToWishlist as AddProductsToWishlistModel; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Adding products to wishlist resolver + */ +class AddProductsToWishlist implements ResolverInterface +{ + /** + * @var AddProductsToWishlistModel + */ + private $addProductsToWishlist; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig + * @param AddProductsToWishlistModel $addProductsToWishlist + * @param WishlistDataMapper $wishlistDataMapper + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig, + AddProductsToWishlistModel $addProductsToWishlist, + WishlistDataMapper $wishlistDataMapper + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; + $this->addProductsToWishlist = $addProductsToWishlist; + $this->wishlistDataMapper = $wishlistDataMapper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if (null === $customerId || 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItems = $this->getWishlistItems($args['wishlistItems']); + $wishlistOutput = $this->addProductsToWishlist->execute($wishlist, $wishlistItems); + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'userInputErrors' => array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get wishlist items + * + * @param array $wishlistItemsData + * + * @return array + */ + private function getWishlistItems(array $wishlistItemsData): array + { + $wishlistItems = []; + + foreach ($wishlistItemsData as $wishlistItemData) { + $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); + } + + return $wishlistItems; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId !== null && $wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php index a84ce0e965b6d..cad574ef56ed2 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php @@ -9,9 +9,11 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; use Magento\Wishlist\Model\WishlistFactory; /** @@ -24,12 +26,21 @@ class CustomerWishlistResolver implements ResolverInterface */ private $wishlistFactory; + /** + * @var WishlistConfig + */ + private $wishlistConfig; + /** * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig */ - public function __construct(WishlistFactory $wishlistFactory) - { + public function __construct( + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig + ) { $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; } /** @@ -42,6 +53,10 @@ public function resolve( array $value = null, array $args = null ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); } diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php new file mode 100644 index 0000000000000..1c741361ea7b7 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\Wishlist\RemoveProductsFromWishlist as RemoveProductsFromWishlistModel; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Removing products from wishlist resolver + */ +class RemoveProductsFromWishlist implements ResolverInterface +{ + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var RemoveProductsFromWishlistModel + */ + private $removeProductsFromWishlist; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @param WishlistFactory $wishlistFactory + * @param WishlistResourceModel $wishlistResource + * @param WishlistConfig $wishlistConfig + * @param WishlistDataMapper $wishlistDataMapper + * @param RemoveProductsFromWishlistModel $removeProductsFromWishlist + */ + public function __construct( + WishlistFactory $wishlistFactory, + WishlistResourceModel $wishlistResource, + WishlistConfig $wishlistConfig, + WishlistDataMapper $wishlistDataMapper, + RemoveProductsFromWishlistModel $removeProductsFromWishlist + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistConfig = $wishlistConfig; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistDataMapper = $wishlistDataMapper; + $this->removeProductsFromWishlist = $removeProductsFromWishlist; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if ($customerId === null || 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItemsIds = $args['wishlistItemsIds']; + $wishlistOutput = $this->removeProductsFromWishlist->execute($wishlist, $wishlistItemsIds); + + if (!empty($wishlistItemsIds)) { + $this->wishlistResource->save($wishlist); + } + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'userInputErrors' => \array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId !== null && $wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php new file mode 100644 index 0000000000000..50a56863596c0 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\Wishlist\UpdateProductsInWishlist as UpdateProductsInWishlistModel; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Update wishlist items resolver + */ +class UpdateProductsInWishlist implements ResolverInterface +{ + /** + * @var UpdateProductsInWishlistModel + */ + private $updateProductsInWishlist; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig + * @param UpdateProductsInWishlistModel $updateProductsInWishlist + * @param WishlistDataMapper $wishlistDataMapper + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig, + UpdateProductsInWishlistModel $updateProductsInWishlist, + WishlistDataMapper $wishlistDataMapper + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; + $this->updateProductsInWishlist = $updateProductsInWishlist; + $this->wishlistDataMapper = $wishlistDataMapper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if (null === $customerId || $customerId === 0) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItems = $args['wishlistItems']; + $wishlistItems = $this->getWishlistItems($wishlistItems); + $wishlistOutput = $this->updateProductsInWishlist->execute($wishlist, $wishlistItems); + + if (count($wishlistOutput->getErrors()) !== count($wishlistItems)) { + $this->wishlistResource->save($wishlist); + } + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'userInputErrors' => \array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get DTO wishlist items + * + * @param array $wishlistItemsData + * + * @return array + */ + private function getWishlistItems(array $wishlistItemsData): array + { + $wishlistItems = []; + + foreach ($wishlistItemsData as $wishlistItemData) { + $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); + } + + return $wishlistItems; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if (null !== $wishlistId && 0 < $wishlistId) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php index 792928ab61aaf..09c0a8a935a6c 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -8,10 +8,12 @@ namespace Magento\WishlistGraphQl\Model\Resolver; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; use Magento\Wishlist\Model\WishlistFactory; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; @@ -30,14 +32,24 @@ class WishlistResolver implements ResolverInterface */ private $wishlistFactory; + /** + * @var WishlistConfig + */ + private $wishlistConfig; + /** * @param WishlistResourceModel $wishlistResource * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig */ - public function __construct(WishlistResourceModel $wishlistResource, WishlistFactory $wishlistFactory) - { + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig + ) { $this->wishlistResource = $wishlistResource; $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; } /** @@ -50,6 +62,10 @@ public function resolve( array $value = null, array $args = null ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + $customerId = $context->getUserId(); /* Guest checking */ diff --git a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php index 8385d3ca852a4..017462b4c94c6 100644 --- a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php +++ b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php @@ -14,6 +14,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\GraphQl\Model\Query\ContextExtensionInterface; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config; use Magento\Wishlist\Model\WishlistFactory; use Magento\WishlistGraphQl\Model\Resolver\CustomerWishlistResolver; use PHPUnit\Framework\MockObject\MockObject; @@ -48,6 +49,11 @@ class CustomerWishlistResolverTest extends TestCase */ private $resolver; + /** + * @var Config|MockObject + */ + private $wishlistConfigMock; + /** * Build the Testing Environment */ @@ -74,9 +80,12 @@ protected function setUp(): void ->setMethods(['loadByCustomerId', 'getId', 'getSharingCode', 'getUpdatedAt', 'getItemsCount']) ->getMock(); + $this->wishlistConfigMock = $this->createMock(Config::class); + $objectManager = new ObjectManager($this); $this->resolver = $objectManager->getObject(CustomerWishlistResolver::class, [ - 'wishlistFactory' => $this->wishlistFactoryMock + 'wishlistFactory' => $this->wishlistFactoryMock, + 'wishlistConfig' => $this->wishlistConfigMock ]); } @@ -85,6 +94,8 @@ protected function setUp(): void */ public function testThrowExceptionWhenUserNotAuthorized(): void { + $this->wishlistConfigMock->method('isEnabled')->willReturn(true); + // Given $this->extensionAttributesMock->method('getIsCustomer') ->willReturn(false); @@ -107,6 +118,8 @@ public function testThrowExceptionWhenUserNotAuthorized(): void */ public function testFactoryCreatesWishlistByAuthorizedCustomerId(): void { + $this->wishlistConfigMock->method('isEnabled')->willReturn(true); + // Given $this->extensionAttributesMock->method('getIsCustomer') ->willReturn(true); diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index deaa66921ba7c..794e90ed9f9a9 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -6,7 +6,7 @@ type Query { } type Customer { - wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "The wishlist query returns the contents of a customer's wish lists") @cache(cacheable: false) + wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "Contains the contents of a customer's wish lists") @cache(cacheable: false) } type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be used instead") { @@ -32,3 +32,45 @@ type WishlistItem { added_at: String @doc(description: "The time when the customer added the item to the wish list"), product: ProductInterface @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\ProductResolver") } + +type Mutation { + addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput @doc(description: "Adds one or more products to the specified wish list. This mutation supports all product types") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\AddProductsToWishlist") + removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput @doc(description: "Removes one or more products from the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\RemoveProductsFromWishlist") + updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput @doc(description: "Updates one or more products in the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\UpdateProductsInWishlist") +} + +input WishlistItemInput @doc(description: "Defines the items to add to a wish list") { + sku: String @doc(description: "The SKU of the product to add. For complex product types, specify the child product SKU") + quantity: Float @doc(description: "The amount or number of items to add") + parent_sku: String @doc(description: "For complex product types, the SKU of the parent product") + selected_options: [String!] @doc(description: "An array of strings corresponding to options the customer selected") + entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered") +} + +type AddProductsToWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully added") + userInputErrors:[CheckoutUserInputError]! @doc(description: "An array of errors encountered while adding products to a wish list") +} + +input EnteredOptionInput @doc(description: "Defines a customer-entered option") { + id: String! @doc(description: "A base64 encoded ID") + value: String! @doc(description: "Text the customer entered") +} + +type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with after items were successfully deleted") + userInputErrors:[CheckoutUserInputError]! @doc(description:"An array of errors encountered while deleting products from a wish list") +} + +input WishlistItemUpdateInput @doc(description: "Defines updates to items in a wish list") { + wishlist_item_id: ID @doc(description: "The ID of the wishlist item to update") + quantity: Float @doc(description: "The new amount or number of this item") + description: String @doc(description: "Describes the update") + selected_options: [String!] @doc(description: "An array of strings corresponding to options the customer selected") + entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered") +} + +type UpdateProductsInWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully updated") + userInputErrors:[CheckoutUserInputError]! @doc(description:"An array of errors encountered while updating products in a wish list") +} diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less index f57420deb621d..4b48bbe99ced2 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less @@ -457,11 +457,26 @@ .action { &.delete { &:extend(.abs-remove-button-for-blocks all); - line-height: unset; position: absolute; right: 0; top: -1px; - width: auto; + } + } + + .block-wishlist { + .action { + &.delete { + line-height: unset; + width: auto; + } + } + } + + .block-compare { + .action { + &.delete { + right: initial; + } } } @@ -814,6 +829,7 @@ &:extend(.abs-remove-button-for-blocks all); left: -6px; position: absolute; + right: 0; top: 0; } diff --git a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less index 09759d95c4b10..8434812f20719 100644 --- a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less @@ -82,6 +82,10 @@ .field { margin-right: 5px; + &.newsletter { + max-width: 220px; + } + .control { width: 100%; } diff --git a/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js b/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js index 87632a6962cc5..cae30c83d95bc 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js +++ b/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js @@ -5,7 +5,6 @@ var config = { deps: [ - 'Magento_Theme/js/responsive', 'Magento_Theme/js/theme' ] }; diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js b/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js deleted file mode 100644 index 011417f54ad9a..0000000000000 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'jquery', - 'matchMedia', - 'mage/tabs', - 'domReady!' -], function ($, mediaCheck) { - 'use strict'; - - mediaCheck({ - media: '(min-width: 768px)', - - /** - * Switch to Desktop Version. - */ - entry: function () { - var galleryElement; - - (function () { - - var productInfoMain = $('.product-info-main'), - productInfoAdditional = $('#product-info-additional'); - - if (productInfoAdditional.length) { - productInfoAdditional.addClass('hidden'); - productInfoMain.removeClass('responsive'); - } - - })(); - - galleryElement = $('[data-role=media-gallery]'); - - if (galleryElement.length && galleryElement.data('mageZoom')) { - galleryElement.zoom('enable'); - } - - if (galleryElement.length && galleryElement.data('mageGallery')) { - galleryElement.gallery('option', 'disableLinks', true); - galleryElement.gallery('option', 'showNav', false); - galleryElement.gallery('option', 'showThumbs', true); - } - }, - - /** - * Switch to Mobile Version. - */ - exit: function () { - var galleryElement; - - $('.action.toggle.checkout.progress').on('click.gotoCheckoutProgress', function () { - var myWrapper = '#checkout-progress-wrapper'; - - scrollTo(myWrapper + ' .title'); - $(myWrapper + ' .title').addClass('active'); - $(myWrapper + ' .content').show(); - }); - - $('body').on('click.checkoutProgress', '#checkout-progress-wrapper .title', function () { - $(this).toggleClass('active'); - $('#checkout-progress-wrapper .content').toggle(); - }); - - galleryElement = $('[data-role=media-gallery]'); - - setTimeout(function () { - if (galleryElement.length && galleryElement.data('mageZoom')) { - galleryElement.zoom('disable'); - } - - if (galleryElement.length && galleryElement.data('mageGallery')) { - galleryElement.gallery('option', 'disableLinks', false); - galleryElement.gallery('option', 'showNav', true); - galleryElement.gallery('option', 'showThumbs', false); - } - }, 2000); - } - }); -}); diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js b/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js index ab8a6063f29a7..e4edd3bd8662c 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js @@ -12,21 +12,9 @@ define([ ], function ($, keyboardHandler) { 'use strict'; - if ($('body').hasClass('checkout-cart-index')) { - if ($('#co-shipping-method-form .fieldset.rates').length > 0 && - $('#co-shipping-method-form .fieldset.rates :checked').length === 0 - ) { - $('#block-shipping').on('collapsiblecreate', function () { - $('#block-shipping').collapsible('forceActivate'); - }); - } - } - $('.cart-summary').mage('sticky', { container: '#maincontent' }); - $('.panel.header > .header.links').clone().appendTo('#store\\.links'); - keyboardHandler.apply(); }); diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index d0b7aa1523ad6..e205b20efd17c 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -998,6 +998,15 @@ } } } + + .block-compare { + .action { + &.delete { + left: 0; + right: initial; + } + } + } } } @@ -1005,6 +1014,7 @@ .compare.wrapper { display: none; } + .catalog-product_compare-index { .columns { .column { diff --git a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less index a72f31d72ce48..21ed451a69d10 100644 --- a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less @@ -81,6 +81,10 @@ .block.newsletter { max-width: 44%; width: max-content; + + .field.newsletter { + max-width: 220px; + } .form.subscribe { > .field, diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html index 024f6daf76ace..e51b952281ed5 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var formattedBillingAddress|raw":"Billing Address", "var order_data.email_customer_note|escape|nl2br":"Email Order Note", -"var order.billing_address.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", @@ -27,7 +27,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send you a tracking number."}} diff --git a/composer.json b/composer.json index 25e6c6c5435bf..1b260cc122865 100644 --- a/composer.json +++ b/composer.json @@ -88,7 +88,7 @@ "friendsofphp/php-cs-fixer": "~2.16.0", "lusitanian/oauth": "~0.8.10", "magento/magento-coding-standard": "*", - "magento/magento2-functional-testing-framework": "3.0.0-RC4", + "magento/magento2-functional-testing-framework": "3.0.0-RC5", "pdepend/pdepend": "~2.7.1", "phpcompatibility/php-compatibility": "^9.3", "phpmd/phpmd": "^2.8.0", diff --git a/composer.lock b/composer.lock index 39282cb149dc6..e5614cfd0ac99 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4abc523fda743ab847f07f9905bb2731", + "content-hash": "f3674961f96b48fdd025a6c94610c8eb", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -206,6 +206,16 @@ "ssl", "tls" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], "time": "2020-04-08T08:27:21+00:00" }, { @@ -452,6 +462,12 @@ "Xdebug", "performance" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + } + ], "time": "2020-03-01T12:26:26+00:00" }, { @@ -3908,6 +3924,20 @@ "x.509", "x509" ], + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], "time": "2020-04-04T23:17:33+00:00" }, { @@ -4444,6 +4474,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -4622,6 +4666,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-30T20:35:19+00:00" }, { @@ -4671,6 +4729,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -5867,6 +5939,12 @@ "functional testing", "unit testing" ], + "funding": [ + { + "url": "https://opencollective.com/codeception", + "type": "open_collective" + } + ], "time": "2020-05-24T13:58:47+00:00" }, { @@ -6439,6 +6517,20 @@ "redis", "xcache" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], "time": "2020-05-27T16:24:54+00:00" }, { @@ -6562,6 +6654,20 @@ "constructor", "instantiate" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], "time": "2020-05-29T17:27:14+00:00" }, { @@ -6624,6 +6730,20 @@ "parser", "php" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], "time": "2020-05-25T17:44:05+00:00" }, { @@ -7083,16 +7203,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.0.0-RC4", + "version": "3.0.0-RC5", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "34781ccc7385993b1e5bc9182e6ddddde7f2769f" + "reference": "e5126f4eb476e227e3b668b622159c917f123175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/34781ccc7385993b1e5bc9182e6ddddde7f2769f", - "reference": "34781ccc7385993b1e5bc9182e6ddddde7f2769f", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/e5126f4eb476e227e3b668b622159c917f123175", + "reference": "e5126f4eb476e227e3b668b622159c917f123175", "shasum": "" }, "require": { @@ -7168,7 +7288,7 @@ "magento", "testing" ], - "time": "2020-06-08T18:17:54+00:00" + "time": "2020-06-15T19:51:46+00:00" }, { "name": "mikey179/vfsstream", @@ -9735,6 +9855,20 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-27T08:34:37+00:00" }, { @@ -9796,6 +9930,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-24T12:18:07+00:00" }, { @@ -9859,6 +10007,20 @@ "mime", "mime-type" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-25T12:33:44+00:00" }, { @@ -10034,6 +10196,20 @@ "portable", "shim" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-12T16:47:27+00:00" }, { @@ -10147,6 +10323,20 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-20T17:43:50+00:00" }, { @@ -10537,5 +10727,6 @@ "ext-zip": "*", "lib-libxml": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "1.1.0" } diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml index f4af91e02b2a1..b0b114890041b 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml @@ -43,4 +43,44 @@ </dataSet> </method> </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesInterface"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <method name="testInterfaceInheritance"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_3" remove="true"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesAbstractClass"> + <method name="testAbstractInheritance"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_2" remove="true"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <dataSet name="first_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + </dataSet> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipAbstractClass"> + <method name="testAbstractSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="first_data_set" skip="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipInterface"> + <method name="testInterfaceSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="second_data_set" skip="true"/> + </method> + </test> </overrides> diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php index 9fa5d2868fd11..06605d156933d 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php @@ -11,6 +11,11 @@ use Magento\Framework\Config\ConverterInterface; use Magento\Framework\Config\SchemaLocatorInterface; use Magento\Framework\View\File\CollectorInterface; +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ApiConfigFixture; +use Magento\TestFramework\Annotation\ApiDataFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; use Magento\TestFramework\WebapiWorkaround\Override\Config\Converter; use Magento\TestFramework\WebapiWorkaround\Override\Config\FileCollector; use Magento\TestFramework\WebapiWorkaround\Override\Config\SchemaLocator; @@ -21,12 +26,23 @@ */ class Config extends IntegrationConfig { + /** + * @inheritdoc + */ + protected const FIXTURE_TYPES = [ + ApiDataFixture::ANNOTATION, + ApiConfigFixture::ANNOTATION, + DataFixture::ANNOTATION, + DataFixtureBeforeTransaction::ANNOTATION, + AdminConfigFixture::ANNOTATION, + ]; + /** * @inheritdoc */ protected function getConverter(): ConverterInterface { - return ObjectManager::getInstance()->create(Converter::class); + return ObjectManager::getInstance()->create(Converter::class, ['types' => $this::FIXTURE_TYPES]); } /** diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php index ce83a611020a8..c14e535187296 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php @@ -8,7 +8,6 @@ namespace Magento\TestFramework\WebapiWorkaround\Override\Config; use Magento\TestFramework\Annotation\AdminConfigFixture; -use Magento\TestFramework\Annotation\ApiConfigFixture; use Magento\TestFramework\Annotation\ApiDataFixture; use Magento\TestFramework\Annotation\ConfigFixture; use Magento\TestFramework\Annotation\DataFixture; @@ -20,14 +19,6 @@ */ class Converter extends IntegrationConverter { - protected const FIXTURE_TYPES = [ - ApiDataFixture::ANNOTATION, - ApiConfigFixture::ANNOTATION, - DataFixture::ANNOTATION, - DataFixtureBeforeTransaction::ANNOTATION, - AdminConfigFixture::ANNOTATION, - ]; - /** * Fill node attributes values * diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php index a00af2d6eb076..f4aedb86c50b1 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php @@ -6,23 +6,26 @@ namespace Magento\Customer\Api; -use Magento\Customer\Api\Data\CustomerInterface as Customer; use Magento\Customer\Api\Data\AddressInterface as Address; +use Magento\Customer\Api\Data\CustomerInterface as Customer; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SortOrder; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Customer as CustomerHelper; use Magento\TestFramework\TestCase\WebapiAbstract; -use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; -use Magento\Framework\Exception\NoSuchEntityException; /** - * Test class for Magento\Customer\Api\CustomerRepositoryInterface + * Test for \Magento\Customer\Api\CustomerRepositoryInterface. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -45,12 +48,12 @@ class CustomerRepositoryTest extends WebapiAbstract private $customerRepository; /** - * @var \Magento\Framework\Api\DataObjectHelper + * @var DataObjectHelper */ private $dataObjectHelper; /** - * @var \Magento\Customer\Api\Data\CustomerInterfaceFactory + * @var CustomerInterfaceFactory */ private $customerDataFactory; @@ -70,7 +73,7 @@ class CustomerRepositoryTest extends WebapiAbstract private $filterGroupBuilder; /** - * @var \Magento\Customer\Model\CustomerRegistry + * @var CustomerRegistry */ private $customerRegistry; @@ -131,7 +134,7 @@ protected function tearDown(): void $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $customerId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -165,24 +168,23 @@ public function testInvalidCustomerUpdate() $customerTokenService = Bootstrap::getObjectManager()->create( \Magento\Integration\Api\CustomerTokenServiceInterface::class ); - $token = $customerTokenService->createCustomerAccessToken($firstCustomerData[Customer::EMAIL], 'test@123'); + $token = $customerTokenService->createCustomerAccessToken( + $firstCustomerData[Customer::EMAIL], + 'test@123' + ); //Create second customer and update lastname. $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); $customerData[Customer::LASTNAME] = $lastName . 'Updated'; $newCustomerDataObject = $this->customerDataFactory->create(); - $this->dataObjectHelper->populateWithArray( - $newCustomerDataObject, - $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class - ); + $this->dataObjectHelper->populateWithArray($newCustomerDataObject, $customerData, Customer::class); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, 'token' => $token, ], 'soap' => [ @@ -195,7 +197,7 @@ public function testInvalidCustomerUpdate() $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; $this->_webApiCall($serviceInfo, $requestData); @@ -209,7 +211,7 @@ public function testDeleteCustomer() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $customerData[Customer::ID], - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -228,16 +230,21 @@ public function testDeleteCustomer() //Verify if the customer is deleted $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); $this->expectExceptionMessage(sprintf("No such entity with customerId = %s", $customerData[Customer::ID])); - $this->_getCustomerData($customerData[Customer::ID]); + $this->getCustomerData($customerData[Customer::ID]); } - public function testDeleteCustomerInvalidCustomerId() + /** + * Test delete customer with invalid id + * + * @return void + */ + public function testDeleteCustomerInvalidCustomerId(): void { $invalidId = -1; $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $invalidId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -266,23 +273,25 @@ public function testDeleteCustomerInvalidCustomerId() } } - public function testUpdateCustomer() + /** + * Test customer update + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testUpdateCustomer(): void { - $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); - $lastName = $existingCustomerDataObject->getLastname(); - $customerData[Customer::LASTNAME] = $lastName . 'Updated'; - $newCustomerDataObject = $this->customerDataFactory->create(); - $this->dataObjectHelper->populateWithArray( - $newCustomerDataObject, - $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class - ); + $customerId = 1; + $updatedLastname = 'Updated lastname'; + $customer = $this->getCustomerData($customerId); + $customerData = $this->dataObjectProcessor->buildOutputDataArray($customer, Customer::class); + $customerData[Customer::LASTNAME] = $updatedLastname; $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'resourcePath' => self::RESOURCE_PATH . '/' . $customerId, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -290,17 +299,18 @@ public function testUpdateCustomer() 'operation' => self::SERVICE_NAME . 'Save', ], ]; - $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( - $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class - ); - $requestData = ['customer' => $newCustomerDataObject]; + + $requestData['customer'] = TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP + ? $customerData + : [Customer::LASTNAME => $updatedLastname]; + $response = $this->_webApiCall($serviceInfo, $requestData); - $this->assertTrue($response !== null); + $this->assertNotNull($response); //Verify if the customer is updated - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); - $this->assertEquals($lastName . "Updated", $existingCustomerDataObject->getLastname()); + $existingCustomerDataObject = $this->getCustomerData($customerId); + $this->assertEquals($updatedLastname, $existingCustomerDataObject->getLastname()); + $this->assertEquals($customerData[Customer::FIRSTNAME], $existingCustomerDataObject->getFirstname()); } /** @@ -309,20 +319,20 @@ public function testUpdateCustomer() public function testUpdateCustomerNoWebsiteId() { $customerData = $this->customerHelper->createSampleCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); $customerData[Customer::LASTNAME] = $lastName . 'Updated'; $newCustomerDataObject = $this->customerDataFactory->create(); $this->dataObjectHelper->populateWithArray( $newCustomerDataObject, $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -332,32 +342,28 @@ public function testUpdateCustomerNoWebsiteId() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); unset($newCustomerDataObject['website_id']); $requestData = ['customer' => $newCustomerDataObject]; - $expectedMessage = '"Associate to Website" is a required value.'; try { - $this->_webApiCall($serviceInfo, $requestData); - $this->fail("Expected exception."); + $response = $this->_webApiCall($serviceInfo, $requestData); + $this->assertEquals($customerData['website_id'], $response['website_id']); } catch (\SoapFault $e) { - $this->assertStringContainsString( - $expectedMessage, - $e->getMessage(), - "SoapFault does not contain expected message." - ); - } catch (\Exception $e) { - $errorObj = $this->customerHelper->processRestExceptionResult($e); - $this->assertEquals($expectedMessage, $errorObj['message'], 'Invalid message: "' . $e->getMessage() . '"'); - $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode()); + $this->assertStringContainsString('"Associate to Website" is a required value.', $e->getMessage()); } } - public function testUpdateCustomerException() + /** + * Test customer exception update + * + * @return void + */ + public function testUpdateCustomerException(): void { $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); //Set non-existent id = -1 @@ -367,13 +373,13 @@ public function testUpdateCustomerException() $this->dataObjectHelper->populateWithArray( $newCustomerDataObject, $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/-1", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -383,7 +389,7 @@ public function testUpdateCustomerException() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; @@ -408,12 +414,14 @@ public function testUpdateCustomerException() /** * Test creating a customer with absent required address fields + * + * @return void */ - public function testCreateCustomerWithoutAddressRequiresException() + public function testCreateCustomerWithoutAddressRequiresException(): void { $customerDataArray = $this->dataObjectProcessor->buildOutputDataArray( $this->customerHelper->createSampleCustomerDataObject(), - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); foreach ($customerDataArray[Customer::KEY_ADDRESSES] as & $address) { @@ -423,7 +431,7 @@ public function testCreateCustomerWithoutAddressRequiresException() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -571,7 +579,7 @@ public function testSearchCustomersUsingGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo); @@ -588,7 +596,7 @@ public function testSearchCustomersUsingGETEmptyFilter() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; try { @@ -640,7 +648,7 @@ public function testSearchCustomersMultipleFiltersWithSort() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -682,7 +690,7 @@ public function testSearchCustomersMultipleFiltersWithSortUsingGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo); @@ -716,7 +724,7 @@ public function testSearchCustomersNonExistentMultipleFilters() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -755,7 +763,7 @@ public function testSearchCustomersNonExistentMultipleFiltersGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo, $requestData); @@ -793,7 +801,7 @@ public function testSearchCustomersMultipleFilterGroups() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -886,11 +894,11 @@ public function testRevokeAllAccessTokensForCustomer() * Retrieve customer data by Id * * @param int $customerId - * @return \Magento\Customer\Api\Data\CustomerInterface + * @return Customer */ - protected function _getCustomerData($customerId) + private function getCustomerData($customerId): Customer { - $customerData = $this->customerRepository->getById($customerId); + $customerData = $this->customerRepository->getById($customerId); $this->customerRegistry->remove($customerId); return $customerData; } diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php new file mode 100644 index 0000000000000..9c7abcd6c8364 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php @@ -0,0 +1,213 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Api; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Registry; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Integration\Model\Oauth\Token as TokenModel; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Customer as CustomerHelper; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + */ +class CustomerSharingOptionsTest extends WebapiAbstract +{ + const RESOURCE_PATH = '/V1/customers/me'; + const REPO_SERVICE = 'customerCustomerRepositoryV1'; + const SERVICE_VERSION = 'V1'; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CustomerHelper + */ + private $customerHelper; + + /** + * @var TokenModel + */ + private $token; + + /** + * @var CustomerInterface + */ + private $customerData; + + /** + * @var CustomerTokenServiceInterface + */ + private $tokenService; + + /** + * Execute per test initialization. + */ + public function setUp(): void + { + $this->customerRegistry = Bootstrap::getObjectManager()->get( + \Magento\Customer\Model\CustomerRegistry::class + ); + + $this->customerRepository = Bootstrap::getObjectManager()->get( + CustomerRepositoryInterface::class, + ['customerRegistry' => $this->customerRegistry] + ); + + $this->customerHelper = new CustomerHelper(); + $this->customerData = $this->customerHelper->createSampleCustomer(); + $this->tokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + + // get token + $this->resetTokenForCustomerSampleData(); + } + + /** + * Ensure that fixture customer and his addresses are deleted. + */ + public function tearDown(): void + { + $this->customerRepository = null; + + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + parent::tearDown(); + } + + /** + * @param string $storeCode + * @param bool $expectingException + * @dataProvider getCustomerDataWebsiteScopeDataProvider + * + * @magentoConfigFixture default_store customer/account_share/scope 1 + */ + public function testGetCustomerDataWebsiteScope(string $storeCode, bool $expectingException) + { + $this->_markTestAsRestOnly('SOAP is difficult to generate exception messages, inconsistencies in WSDL'); + $this->processGetCustomerData($storeCode, $expectingException); + } + + /** + * @param string $storeCode + * @param bool $expectingException + * @dataProvider getCustomerDataGlobalScopeDataProvider + * + * @magentoConfigFixture customer/account_share/scope 0 + */ + public function testGetCustomerDataGlobalScope(string $storeCode, bool $expectingException) + { + $this->processGetCustomerData($storeCode, $expectingException); + } + + /** + * @param string $storeCode + * @param bool $expectingException + */ + private function processGetCustomerData(string $storeCode, bool $expectingException) + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_GET, + 'token' => $this->token, + ], + 'soap' => [ + 'service' => self::REPO_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::REPO_SERVICE . 'GetSelf', + 'token' => $this->token + ] + ]; + $arguments = []; + if (TESTS_WEB_API_ADAPTER === 'soap') { + $arguments['customerId'] = 0; + } + + if ($expectingException) { + self::expectException(\Exception::class); + self::expectExceptionMessage("The consumer isn't authorized to access %resources."); + } + + $this->_webApiCall($serviceInfo, $arguments, null, $storeCode); + } + + /** + * Data provider for testGetCustomerDataWebsiteScope. + * + * @return array + */ + public function getCustomerDataWebsiteScopeDataProvider(): array + { + return [ + 'Default Store View' => [ + 'store_code' => 'default', + 'exception' => false + ], + 'Custom Store View' => [ + 'store_code' => 'fixture_second_store', + 'exception' => true + ] + ]; + } + + /** + * Data provider for testGetCustomerDataGlobalScope. + * + * @return array + */ + public function getCustomerDataGlobalScopeDataProvider(): array + { + return [ + 'Default Store View' => [ + 'store_code' => 'default', + 'exception' => false + ], + 'Custom Store View' => [ + 'store_code' => 'fixture_second_store', + 'exception' => false + ] + ]; + } + + /** + * Sets the test's access token for the created customer sample data + */ + private function resetTokenForCustomerSampleData() + { + $this->resetTokenForCustomer($this->customerData[CustomerInterface::EMAIL], 'test@123'); + } + + /** + * Sets the test's access token for a particular username and password. + * + * @param string $username + * @param string $password + */ + private function resetTokenForCustomer($username, $password) + { + $this->token = $this->tokenService->createCustomerAccessToken($username, $password); + $this->customerRegistry->remove($this->customerRepository->get($username)->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index 00eb235cb4dc3..d6477c82513e9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -714,4 +714,60 @@ private function assertCategoryChildren(array $category, array $expectedChildren $this->assertResponseFields($category['children'][$i], $expectedChild); } } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryInlineFragment() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {eq: "6"}}){ + ... on CategoryTree { + id + name + url_key + url_path + children_count + path + position + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryNamedFragment() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {eq: "6"}}){ + ...Cat + } +} + +fragment Cat on CategoryTree { + id + name + url_key + url_path + children_count + path + position +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php new file mode 100644 index 0000000000000..97c6c41ad6397 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Exception; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductAttributeStoreOptionsTest extends GraphQlAbstract +{ + /** + * Test that custom attribute option labels are returned respecting store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php + * @throws LocalizedException + */ + public function testAttributeStoreLabels(): void + { + $this->attributeLabelTest('Option Default Store'); + $this->attributeLabelTest('Option Test Store', ['Store' => 'test']); + } + + /** + * @param $expectedLabel + * @param array $headers + * @throws LocalizedException + * @throws Exception + */ + private function attributeLabelTest($expectedLabel, array $headers = []): void + { + /** @var Config $eavConfig */ + $eavConfig = Bootstrap::getObjectManager()->get(Config::class); + $attributeCode = 'test_configurable'; + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $optionValues = []; + + foreach ($options as $option) { + $optionValues[] = [ + 'value' => $option->getValue(), + ]; + } + + $expectedOptions = [ + [ + 'label' => $expectedLabel, + 'value' => $optionValues[0]['value'] + ] + ]; + + $query = <<<QUERY +{ + products(search:"Simple", + pageSize: 3 + currentPage: 1 + ) + { + aggregations + { + attribute_code + options + { + label + value + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertNotEmpty($response['products']['aggregations']); + $actualAttributes = $response['products']['aggregations']; + $actualAttributeOptions = []; + + foreach ($actualAttributes as $actualAttribute) { + if ($actualAttribute['attribute_code'] === $attributeCode) { + $actualAttributeOptions = $actualAttribute['options']; + } + } + + $this->assertNotEmpty($actualAttributeOptions); + + foreach ($actualAttributeOptions as $key => $actualAttributeOption) { + if ($actualAttributeOption['value'] === $expectedOptions[$key]['value']) { + $this->assertEquals($actualAttributeOption['label'], $expectedOptions[$key]['label']); + } + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 1a95a3d6f4925..dd5b5827c8017 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -570,8 +570,8 @@ public function testSearchAndFilterByCustomAttribute() ], [ 'count' => 1, - 'label' => '40-*', - 'value' => '40_*', + 'label' => '40-50', + 'value' => '40_50', ], ], @@ -1431,8 +1431,8 @@ public function testFilterProductsForExactMatchingName() 'count' => 1, ], [ - 'label' => '20-*', - 'value' => '20_*', + 'label' => '20-30', + 'value' => '20_30', 'count' => 1, ] ] diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index 99fdfb2cf1b00..c6719f1862ddc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -665,7 +665,8 @@ private function assertMediaGalleryEntries($product, $actualResponse) { $mediaGalleryEntries = $product->getMediaGalleryEntries(); $this->assertCount(1, $mediaGalleryEntries, "Precondition failed, incorrect number of media gallery entries."); - $this->assertIsArray([$actualResponse['media_gallery_entries']], + $this->assertIsArray( + [$actualResponse['media_gallery_entries']], "Media galleries field must be of an array type." ); $this->assertCount(1, $actualResponse['media_gallery_entries'], "There must be 1 record in media gallery."); @@ -701,10 +702,10 @@ private function assertMediaGalleryEntries($product, $actualResponse) */ private function assertCustomAttribute($actualResponse) { - $customAttribute = null; + $customAttribute = 'customAttributeValue'; $this->assertEquals($customAttribute, $actualResponse['attribute_code_custom']); } - + /** * @param ProductInterface $product * @param $actualResponse diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php new file mode 100644 index 0000000000000..25c808a549e80 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\GraphQl\Model\Cors\Configuration; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class CorsHeadersTest extends GraphQlAbstract +{ + /** + * @var Config $config + */ + private $resourceConfig; + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = ObjectManager::getInstance(); + + $this->resourceConfig = $objectManager->get(Config::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->reinitConfig->reinit(); + } + + public function testNoCorsHeadersWhenCorsIsDisabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + public function testCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertEquals('Origin', $headers['Access-Control-Allow-Headers']); + self::assertEquals('1', $headers['Access-Control-Allow-Credentials']); + self::assertEquals('GET,POST', $headers['Access-Control-Allow-Methods']); + self::assertEquals('magento.local', $headers['Access-Control-Allow-Origin']); + self::assertEquals('86400', $headers['Access-Control-Max-Age']); + } + + public function testEmptyCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, ''); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + private function getHeadersFromIntrospectionQuery(): array + { + $query + = <<<QUERY + query IntrospectionQuery { + __schema { + types { + name + } + } + } +QUERY; + + return $this->graphQlQueryWithResponseHeaders($query)['headers'] ?? []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index c42450d86fd58..8d6bae35de49b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -368,6 +368,32 @@ public function testEmptyCustomerLastName() $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerWithIncorrectGender() + { + $gender = 5; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('"' . $gender . '" is not a valid gender value.'); + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + gender: {$gender} + } + ) { + customer { + gender + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php new file mode 100644 index 0000000000000..8b51d37b50a27 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +/** + * Get authentication header for customer + */ +class GetCustomerAuthenticationHeader +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @param CustomerTokenServiceInterface $customerTokenService + */ + public function __construct(CustomerTokenServiceInterface $customerTokenService) + { + $this->customerTokenService = $customerTokenService; + } + + /** + * Get header to perform customer authenticated request + * + * @param string $email + * @param string $password + * @return string[] + * @throws AuthenticationException + */ + public function execute(string $email = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php new file mode 100644 index 0000000000000..fa0909d556b3a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\GiftMessage\Cart\Item; + +use Exception; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class GiftMessageTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageCartForItemNotAllow() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message'); + foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertNull($item['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageCartForItem() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message'); + foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertArrayHasKey('to', $item['gift_message']); + self::assertArrayHasKey('from', $item['gift_message']); + self::assertArrayHasKey('message', $item['gift_message']); + } + } + + /** + * @param string $quoteId + * + * @return array|bool|float|int|string + * @throws Exception + */ + private function requestCartResult(string $quoteId) + { + $query = <<<QUERY +{ + cart(cart_id: "$quoteId") { + items { + product { + name + } + ... on SimpleCartItem { + gift_message { + to + from + message + } + } + } + } +} +QUERY; + return $this->graphQlQuery($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php index e6db0b9e808ef..8cb0a6db972b4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php @@ -7,12 +7,29 @@ namespace Magento\GraphQl\GroupedProduct; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; +/** + * Class to test GraphQl response with grouped products + */ class GroupedProductViewTest extends GraphQlAbstract { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + } /** * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped.php @@ -20,17 +37,16 @@ class GroupedProductViewTest extends GraphQlAbstract public function testAllFieldsGroupedProduct() { $productSku = 'grouped-product'; - $query - = <<<QUERY + $query = <<<QUERY { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { id attribute_set_id created_at name sku - type_id + type_id ... on GroupedProduct { items{ qty @@ -39,9 +55,14 @@ public function testAllFieldsGroupedProduct() sku name type_id - url_key + url_key } } + product_links{ + linked_product_sku + position + link_type + } } } } @@ -49,47 +70,77 @@ public function testAllFieldsGroupedProduct() QUERY; $response = $this->graphQlQuery($query); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $groupedProduct = $productRepository->get($productSku, false, null, true); + $groupedProduct = $this->productRepository->get($productSku, false, null, true); - $this->assertGroupedProductItems($groupedProduct, $response['products']['items'][0]); + $this->assertNotEmpty( + $response['products']['items'][0]['items'], + "Precondition failed: 'Grouped product items' must not be empty" + ); + $this->assertGroupedProductItems($groupedProduct, $response['products']['items'][0]['items']); + $this->assertNotEmpty( + $response['products']['items'][0]['product_links'], + "Precondition failed: 'Linked product items' must not be empty" + ); + $this->assertProductLinks($groupedProduct, $response['products']['items'][0]['product_links']); } - private function assertGroupedProductItems($product, $actualResponse) + /** + * @param ProductInterface $product + * @param array $items + */ + private function assertGroupedProductItems(ProductInterface $product, array $items): void { - $this->assertNotEmpty( - $actualResponse['items'], - "Precondition failed: 'grouped product items' must not be empty" - ); - $this->assertCount(2, $actualResponse['items']); + $this->assertCount(2, $items); $groupedProductLinks = $product->getProductLinks(); - foreach ($actualResponse['items'] as $itemIndex => $bundleItems) { - $this->assertNotEmpty($bundleItems); + foreach ($items as $itemIndex => $bundleItem) { + $this->assertNotEmpty($bundleItem); $associatedProductSku = $groupedProductLinks[$itemIndex]->getLinkedProductSku(); - - $productsRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $associatedProduct */ - $associatedProduct = $productsRepository->get($associatedProductSku); + $associatedProduct = $this->productRepository->get($associatedProductSku); $this->assertEquals( $groupedProductLinks[$itemIndex]->getExtensionAttributes()->getQty(), - $actualResponse['items'][$itemIndex]['qty'] + $bundleItem['qty'] ); $this->assertEquals( $groupedProductLinks[$itemIndex]->getPosition(), - $actualResponse['items'][$itemIndex]['position'] + $bundleItem['position'] ); $this->assertResponseFields( - $actualResponse['items'][$itemIndex]['product'], + $bundleItem['product'], [ - 'sku' => $associatedProductSku, - 'type_id' => $groupedProductLinks[$itemIndex]->getLinkedProductType(), - 'url_key'=> $associatedProduct->getUrlKey(), - 'name' => $associatedProduct->getName() + 'sku' => $associatedProductSku, + 'type_id' => $groupedProductLinks[$itemIndex]->getLinkedProductType(), + 'url_key'=> $associatedProduct->getUrlKey(), + 'name' => $associatedProduct->getName() ] ); } } + + /** + * @param ProductInterface $product + * @param array $links + * @return void + */ + private function assertProductLinks(ProductInterface $product, array $links): void + { + $this->assertCount(2, $links); + $productLinks = $product->getProductLinks(); + foreach ($links as $itemIndex => $linkedItem) { + $this->assertNotEmpty($linkedItem); + $this->assertEquals( + $productLinks[$itemIndex]->getPosition(), + $linkedItem['position'] + ); + $this->assertEquals( + $productLinks[$itemIndex]->getLinkedProductSku(), + $linkedItem['linked_product_sku'] + ); + $this->assertEquals( + $productLinks[$itemIndex]->getLinkType(), + $linkedItem['link_type'] + ); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php index 21a8d6ae94312..ff8d4f4280c10 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php @@ -281,6 +281,50 @@ public function testSetDisabledPaymentOnCart() $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.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_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWitMissingCartId() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = ""; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"cart_id\" is missing" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.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_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithMissingPaymentMethod() + { + $methodCode = ""; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"code\" for \"payment_method\" is missing." + ); + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $methodCode 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 3e06b89c77fb7..900a2877e8c7b 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 @@ -1745,6 +1745,57 @@ public function testSetNewShippingAddressWithDefaultValueOfSaveInAddressBookAndP } } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetShippingAddressOnCartWithNullCustomerAddressId() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: null + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + label + code + } + region { + code + label + } + __typename + } + } + } +} +QUERY; + $this->expectExceptionMessage( + 'The shipping address must contain either "customer_address_id" or "address".' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php index dbc10700794fa..78691d8cbd889 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php @@ -218,6 +218,50 @@ public function testSetDisabledPaymentOnCart() $this->graphQlMutation($query); } + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.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_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWitMissingCartId() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = ""; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"cart_id\" is missing" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.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_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithMissingPaymentMethod() + { + $methodCode = ""; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"code\" for \"payment_method\" is missing." + ); + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $methodCode diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php index a17bc1aa3821a..0a22f3ca9721c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php @@ -7,8 +7,9 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Quote\Model\QuoteFactory; +use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; @@ -273,6 +274,81 @@ private function getCartQuery(string $maskedQuoteId) } } } +QUERY; + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws Exception + */ + public function testUpdateGiftMessageCartForItemNotAllow() + { + $query = $this->getUpdateGiftMessageQuery(); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertNull($item['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws Exception + */ + public function testUpdateGiftMessageCartForItem() + { + $query = $this->getUpdateGiftMessageQuery(); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertSame('Alex', $item['gift_message']['to']); + self::assertSame('Mike', $item['gift_message']['from']); + self::assertSame('Best regards.', $item['gift_message']['message']); + } + } + + private function getUpdateGiftMessageQuery() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_guest_order_with_gift_message', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId(); + + return <<<QUERY +mutation { + updateCartItems( + input: { + cart_id: "$maskedQuoteId", + cart_items: [ + { + cart_item_id: $itemId + quantity: 3 + gift_message: { + to: "Alex" + from: "Mike" + message: "Best regards." + } + } + ] + } + ) { + cart { + items { + id + product { + name + } + quantity + ... on SimpleCartItem { + gift_message { + to + from + message + } + } + } + } + } +} QUERY; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php new file mode 100644 index 0000000000000..db4b2c31a7f48 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php @@ -0,0 +1,434 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\GraphQl\GetCustomerAuthenticationHeader; + +/** + * Tests the Invoice query + */ +class InvoiceTest extends GraphQlAbstract +{ + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + protected function setUp(): void + { + $this->customerAuthenticationHeader + = Bootstrap::getObjectManager()->get(GetCustomerAuthenticationHeader::class); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSingleInvoiceForLoggedInCustomerQuery() + { + $query = + <<<QUERY +query { + customer + { + orders { + items { + order_number + grand_total + status + invoices { + items{ + product_name + product_sku + product_sale_price { + value + } + quantity_invoiced + } + total { + subtotal { + value + } + grand_total { + value + } + total_shipping { + value + } + shipping_handling { + total_amount { + value + } + } + } + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $expectedOrdersData = [ + 'order_number' => '100000001', + 'status' => 'Processing', + 'grand_total' => 100.00 + ]; + + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Related Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10 + ], + 'quantity_invoiced' => 1 + ], + [ + 'product_name' => 'Simple Product With Related Product', + 'product_sku' => 'simple_with_cross', + 'product_sale_price' => [ + 'value' => 10 + ], + 'quantity_invoiced' => 1 + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 100 + ], + 'grand_total' => [ + 'value' => 100 + ], + 'total_shipping' => [ + 'value' => 0 + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => null + ] + ] + ] + ] + ]; + + $actualData = $response['customer']['orders']['items'][0]; + + $this->assertEquals( + $expectedOrdersData['order_number'], + $actualData['order_number'], + "order_number is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $this->assertEquals( + $expectedOrdersData['grand_total'], + $actualData['grand_total'], + "grand_total is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $this->assertEquals( + $expectedOrdersData['status'], + $actualData['status'], + "status is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $invoices = $actualData['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testMultipleInvoiceForLoggedInCustomerQuery() + { + $query = + <<<QUERY +query { + customer + { + orders { + items { + order_number + grand_total + status + invoices { + items{ + product_name + product_sku + product_sale_price { + value + } + quantity_invoiced + } + total { + subtotal { + value + } + grand_total { + value + } + total_shipping { + value + currency + } + shipping_handling { + total_amount { + value + currency + } + amount_including_tax { + value + currency + } + amount_excluding_tax { + value + currency + } + } + } + } +} +} +} +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $expectedOrdersData = [ + 'order_number' => '100000002', + 'status' => 'Processing', + 'grand_total' => 50.00 + ]; + + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Related Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10 + ], + 'quantity_invoiced' => 3 + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 30 + ], + 'grand_total' => [ + 'value' => 50 + ], + 'total_shipping' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'amount_including_tax' => [ + 'value' => 25, + 'currency' => 'USD' + ], + 'amount_excluding_tax' => [ + 'value' => 20, + 'currency' => 'USD' + ] + ] + ] + ], + [ + 'items' => [ + [ + 'product_name' => 'Simple Product With Related Product', + 'product_sku' => 'simple_with_cross', + 'product_sale_price' => [ + 'value' => 10 + ], + 'quantity_invoiced' => 1 + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 10 + ], + 'grand_total' => [ + 'value' => 10 + ], + 'total_shipping' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_including_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_excluding_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ] + ] + ] + ] + ]; + + $actualData = $response['customer']['orders']['items'][0]; + $this->assertEquals( + $expectedOrdersData['order_number'], + $actualData['order_number'], + "order_number is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $this->assertEquals( + $expectedOrdersData['grand_total'], + $actualData['grand_total'], + "grand_total is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $this->assertEquals( + $expectedOrdersData['status'], + $actualData['status'], + "status is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $invoices = $actualData['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customers_with_invoices.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testMultipleCustomersWithInvoicesQuery() + { + $query = + <<<QUERY +query { + customer + { + orders { + items { + order_number + grand_total + status + invoices { + items{ + product_name + product_sku + product_sale_price { + value + currency + } + quantity_invoiced + } + total { + subtotal { + value + currency + } + grand_total { + value + currency + } + total_shipping { + value + currency + } + } + } +} +} +} +} +QUERY; + + $currentEmail = 'customer@search.example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $expectedOrdersData = [ + 'order_number' => '100000001', + 'status' => 'Processing', + 'grand_total' => 100.00 + ]; + + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1 + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 0, + 'currency' => 'USD' + ] + ] + ] + ]; + + $actualData = $response['customer']['orders']['items'][0]; + $this->assertEquals( + $expectedOrdersData['order_number'], + $actualData['order_number'], + "order_number is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $this->assertEquals( + $expectedOrdersData['grand_total'], + $actualData['grand_total'], + "grand_total is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $this->assertEquals( + $expectedOrdersData['status'], + $actualData['status'], + "status is different than the expected for order - " . $expectedOrdersData['order_number'] + ); + $invoices = $actualData['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php new file mode 100644 index 0000000000000..ed7cda3b46ebc --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php @@ -0,0 +1,1278 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Registry; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Class RetrieveOrdersTest + */ +class RetrieveOrdersByOrderNumberTest extends GraphQlAbstract +{ + /** @var OrderRepositoryInterface */ + private $orderRepository; + + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + protected function setUp():void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthenticationHeader = $objectManager->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerOrdersSimpleProductQuery() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"100000002"}}){ + total_count + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + product_sale_price{currency value} + } + total { + base_grand_total { + value + currency + } + grand_total { + value + currency + } + subtotal { + value + currency + } + + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertNotEmpty($response['customer']['orders']['items']); + $customerOrderItemsInResponse = $response['customer']['orders']['items'][0]; + $this->assertArrayHasKey('items', $customerOrderItemsInResponse); + $this->assertNotEmpty($customerOrderItemsInResponse['items']); + + $searchCriteria = $this->searchCriteriaBuilder->addFilter('increment_id', '100000002') + ->create(); + /** @var \Magento\Sales\Api\Data\OrderInterface[] $orders */ + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + foreach ($orders as $order) { + $orderNumber = $order->getIncrementId(); + $this->assertNotEmpty($customerOrderItemsInResponse['id']); + $this->assertEquals($orderNumber, $customerOrderItemsInResponse['number']); + $this->assertEquals('Processing', $customerOrderItemsInResponse['status']); + } + $expectedOrderItems = [ + 'quantity_ordered'=> 2, + 'product_sku'=> 'simple', + 'product_name'=> 'Simple Product', + 'product_sale_price'=> ['currency'=> 'USD', 'value'=> 10] + ]; + $actualOrderItemsFromResponse = $customerOrderItemsInResponse['items'][0]; + $this->assertEquals($expectedOrderItems, $actualOrderItemsFromResponse); + $actualOrderTotalFromResponse = $response['customer']['orders']['items'][0]['total']; + $expectedOrderTotal = [ + 'base_grand_total' => ['value'=> 120,'currency' =>'USD'], + 'grand_total' => ['value'=> 120,'currency' =>'USD'], + 'subtotal' => ['value'=> 120,'currency' =>'USD'] + ]; + $this->assertEquals($expectedOrderTotal, $actualOrderTotalFromResponse, 'Totals do not match'); + } + + /** + * Verify the customer order with tax, discount with shipping tax class set for calculation setting + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrdersSimpleProductWithTaxesAndDiscounts() + { + $quantity = 4; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + // Asserting discounts on order item level + $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $customerOrderResponse[0]['items'][0]['discounts'][0]['label'] + ); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsWithTaxesAndDiscounts($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsWithTaxesAndDiscounts(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(4.05, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 58.05, 'currency' =>'USD'], + 'grand_total' => ['value' => 58.05, 'currency' =>'USD'], + 'subtotal' => ['value' => 40, 'currency' =>'USD'], + 'total_tax' => ['value' => 4.05, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 21.5], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 6, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Verify the customer order with tax, discount with shipping tax class set for calculation setting + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrdersSimpleProductWithTaxesAndDiscountsWithTwoRules() + { + $quantity = 4; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + // Asserting discounts on order item level + $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $customerOrderResponse[0]['items'][0]['discounts'][0]['label'] + ); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsWithTaxesAndDiscountsWithTwoRules($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsWithTaxesAndDiscountsWithTwoRules(array $customerOrderItemTotal): void + { + $this->assertCount(2, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(4.05, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + $secondTaxData = $customerOrderItemTotal['taxes'][1]; + $this->assertEquals('USD', $secondTaxData['amount']['currency']); + $this->assertEquals(2.97, $secondTaxData['amount']['value']); + $this->assertEquals('US-AL-*-Rate-1', $secondTaxData['title']); + $this->assertEquals(5.5, $secondTaxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 61.02, 'currency' =>'USD'], + 'grand_total' => ['value' => 61.02, 'currency' =>'USD'], + 'subtotal' => ['value' => 40, 'currency' =>'USD'], + 'total_tax' => ['value' => 7.02, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 22.6], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ], + 1 => [ + 'amount'=>['value' => 0.99], + 'title' => 'US-AL-*-Rate-1', + 'rate' => 5.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 6, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetMatchingCustomerOrders() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{match:"100"}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + product_type + product_sale_price{currency value} + product_url_key + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(6, $response['customer']['orders']['total_count']); + $this->assertCount(6, $response['customer']['orders']['items']); + $customerOrderItems = $response['customer']['orders']['items']; + $expectedOrderNumbers = ['100000002', '100000004', '100000005','100000006', '100000007', '100000008']; + $actualOrdersFromResponse = []; + foreach ($customerOrderItems as $order) { + array_push($actualOrdersFromResponse, $order['number']); + } + $this->assertEquals($expectedOrderNumbers, $actualOrdersFromResponse, 'Order numbers do not match'); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetMatchingOrdersForLowerQueryLength() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{match:"0"}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + } + } + } +} +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + //character length should not trigger an exception + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(6, $response['customer']['orders']['total_count']); + $this->assertCount($response['customer']['orders']['total_count'], $response['customer']['orders']['items']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testGetMultipleCustomerOrdersQueryWithDefaultPagination() + { + $orderNumbers = ['100000007', '100000008']; + $query = <<<QUERY +{ + customer + { + orders(filter:{number:{in:["{$orderNumbers[0]}","{$orderNumbers[1]}"]}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + product_type + product_sale_price{currency value} + } + total{ + base_grand_total {value currency} + grand_total {value currency} + subtotal {value currency} + total_shipping{value} + total_tax{value currency} + taxes {amount {currency value} title rate} + total_shipping{value} + shipping_handling{ + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value} + taxes {amount{value} title rate} + } + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayNotHasKey('errors', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(2, $response['customer']['orders']['total_count']); + $this->assertArrayHasKey('page_info', $response['customer']['orders']); + $pageInfo = $response['customer']['orders']['page_info']; + $this->assertEquals(1, $pageInfo['current_page']); + $this->assertEquals(20, $pageInfo['page_size']); + $this->assertEquals(1, $pageInfo['total_pages']); + $this->assertNotEmpty($response['customer']['orders']['items']); + $customerOrderItemsInResponse = $response['customer']['orders']['items']; + $this->assertCount(2, $response['customer']['orders']['items']); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('increment_id', $orderNumbers, 'in') + ->create(); + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + $key = 0; + foreach ($orders as $order) { + $orderId = base64_encode($order->getEntityId()); + $orderNumber = $order->getIncrementId(); + $orderItemInResponse = $customerOrderItemsInResponse[$key]; + $this->assertNotEmpty($orderItemInResponse['id']); + $this->assertEquals($orderId, $orderItemInResponse['id']); + $this->assertEquals($orderNumber, $orderItemInResponse['number']); + $this->assertEquals('Processing', $orderItemInResponse['status']); + $this->assertEquals(5, $orderItemInResponse['total']['shipping_handling']['total_amount']['value']); + $this->assertEquals(5, $orderItemInResponse['total']['total_shipping']['value']); + $this->assertEquals(5, $orderItemInResponse['total']['total_tax']['value']); + $key++; + } + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerOrdersUnauthorizedCustomer() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"100000001"}}){ + total_count + items + { + id + number + status + order_date + } + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The current customer isn\'t authorized.'); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @magentoApiDataFixture Magento/Sales/_files/two_orders_for_two_diff_customers.php + */ + public function testGetCustomerOrdersWithWrongCustomer() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"100000001"}}){ + total_count + items + { + id + number + status + order_date + } + } + } +} +QUERY; + $currentEmail = 'customer_two@example.com'; + $currentPassword = 'password'; + $responseWithWrongCustomer = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertEquals(0, $responseWithWrongCustomer['customer']['orders']['total_count']); + $this->assertEmpty($responseWithWrongCustomer['customer']['orders']['items']); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $responseWithCorrectCustomer = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertEquals(1, $responseWithCorrectCustomer['customer']['orders']['total_count']); + $this->assertNotEmpty($responseWithCorrectCustomer['customer']['orders']['items']); + } + + /** + * @param String $orderNumber + * @throws AuthenticationException + * @dataProvider dataProviderIncorrectOrder + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerNonExistingOrderQuery(string $orderNumber) + { + $query = + <<<QUERY +{ + customer { + orders(filter: {number: {eq: "{$orderNumber}"}}) { + items { + number + items { + product_sku + } + total { + base_grand_total { + value + currency + } + grand_total { + value + currency + } + total_shipping { + value + } + shipping_handling { + amount_including_tax { + value + } + amount_excluding_tax { + value + } + total_amount { + value + } + taxes { + amount { + value + } + title + rate + } + } + subtotal { + value + currency + } + taxes { + amount { + value + currency + } + title + rate + } + discounts { + amount { + value + currency + } + label + } + } + } + page_info { + current_page + page_size + total_pages + } + total_count + } + } +} + +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayNotHasKey('errors', $response); + $this->assertArrayHasKey('customer', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertCount(0, $response['customer']['orders']['items']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(0, $response['customer']['orders']['total_count']); + $this->assertArrayHasKey('page_info', $response['customer']['orders']); + $this->assertEquals( + ['current_page' => 1, 'page_size' => 20, 'total_pages' => 0], + $response['customer']['orders']['page_info'] + ); + } + + /** + * @return array + */ + public function dataProviderIncorrectOrder(): array + { + return [ + 'correctFormatNonExistingOrder' => [ + '200000009', + ], + 'alphaFormatNonExistingOrder' => [ + '200AA00B9', + ], + 'longerFormatNonExistingOrder' => [ + 'X0000-0033331', + ], + ]; + } + + /** + * @param String $orderNumber + * @param String $store + * @param int $expectedCount + * @throws AuthenticationException + * @dataProvider dataProviderMultiStores + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php + */ + public function testGetCustomerOrdersTwoStoreViewQuery(string $orderNumber, string $store, int $expectedCount) + { + $query = + <<<QUERY +{ + customer { + orders(filter:{number:{eq:"{$orderNumber}"}}) { + page_info {current_page page_size total_pages} + total_count + items { + number + items{ product_sku } + total { + base_grand_total{value currency} + grand_total{value currency} + subtotal { value currency } + shipping_handling + { + total_amount{value currency} + } + } + } + } + } + } +QUERY; + + $headers = array_merge( + $this->customerAuthenticationHeader->execute('customer@example.com', 'password'), + ['Store' => $store] + ); + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertArrayHasKey('customer', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertCount($expectedCount, $response['customer']['orders']['items']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals($expectedCount, (int)$response['customer']['orders']['total_count']); + $this->assertTotals($response, $expectedCount); + } + + /** + * @param array $response + * @param int $expectedCount + */ + private function assertTotals(array $response, int $expectedCount): void + { + $assertionMap = [ + 'base_grand_total' => ['value' => 100, 'currency' =>'USD'], + 'grand_total' => ['value' => 100, 'currency' =>'USD'], + 'subtotal' => ['value' => 110, 'currency' =>'USD'], + 'shipping_handling' => [ + 'total_amount' => ['value' => 10, 'currency' =>'USD'] + ] + ]; + if ($expectedCount === 0) { + $this->assertEmpty($response['customer']['orders']['items']); + } else { + $customerOrderItemTotal = $response['customer']['orders']['items'][0]['total']; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + } + + /** + * @return array + */ + public function dataProviderMultiStores(): array + { + return [ + 'firstStoreFirstOrder' => [ + '100000001', 'default', 1 + ], + 'secondStoreSecondOrder' => [ + '100000002', 'fixture_second_store', 1 + ], + 'firstStoreSecondOrder' => [ + '100000002', 'default', 0 + ], + 'secondStoreFirstOrder' => [ + '100000001', 'fixture_second_store', 0 + ], + ]; + } + + /** + * Verify that the customer order has the tax information on shipping and totals + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrderWithTaxesExcludedOnShipping() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsAndShippingWithExcludedTaxSetting($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * Assert totals and shipping amounts with taxes excluded + * + * @param $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithExcludedTaxSetting($customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.25, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'total_tax' => ['value' => 2.25, 'currency' =>'USD'], + 'subtotal' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75], + 'amount_excluding_tax' => ['value' => 10], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.75], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts' =>[] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Verify that the customer order has the tax information on shipping and totals + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php + */ + public function testCustomerOrderWithTaxesIncludedOnShippingAndTotals() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsAndShippingWithTaxes($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * Check order totals an shipping amounts with taxes + * + * @param array $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithTaxes(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.25, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + unset($customerOrderItemTotal['shipping_handling']['discounts']); + $assertionMap = [ + 'base_grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'total_tax' => ['value' => 2.25, 'currency' =>'USD'], + 'subtotal' => ['value' => 20, 'currency' =>'USD'], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75], + 'amount_excluding_tax' => ['value' => 10], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.75], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Create an empty cart with GraphQl mutation + * + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['createEmptyCart']; + } + + /** + * Add product to cart with GraphQl query + * + * @param string $cartId + * @param float $qty + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $qty, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cart_items: [ + { + data: { + quantity: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart {items{quantity product {sku}}}} +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set billing address on cart with GraphQL mutation + * + * @param string $cartId + * @return void + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "John" + lastname: "Smith" + company: "Test company" + street: ["test street 1", "test street 2"] + city: "Texas City" + postcode: "78717" + telephone: "5123456677" + region: "TX" + country_code: "US" + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set shipping address on cart with GraphQl query + * + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test shipFirst" + lastname: "test shipLast" + company: "test company" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36013" + country_code: "US" + telephone: "3347665522" + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount {value} + } + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + return $availableShippingMethod; + } + + /** + * Set shipping method on cart with GraphQl mutation + * + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + return $availablePaymentMethod; + } + + /** + * Set payment method on cart with GrpahQl mutation + * + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart {selected_payment_method {code}} + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Place order using GraphQl mutation + * + * @param string $cartId + * @return string + */ + private function placeOrder(string $cartId): string + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_number + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['placeOrder']['order']['order_number']; + } + + /** + * Get customer order query + * + * @param string $orderNumber + * @return array + */ + private function getCustomerOrderQuery($orderNumber): array + { + $query = + <<<QUERY +{ + customer { + email + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items { + id + number + order_date + status + items{product_name product_sku quantity_ordered discounts {amount{value currency} label}} + total { + base_grand_total{value currency} + grand_total{value currency} + total_tax{value currency} + subtotal { value currency } + taxes {amount{value currency} title rate} + discounts {amount{value currency} label} + total_shipping{value currency} + shipping_handling + { + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value currency} + taxes {amount{value} title rate} + discounts {amount{value currency} label} + } + + } + } + } + } + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + return $response['customer']['orders']['items']; + } + + /** + * Clean up orders + * + * @return void + */ + private function deleteOrder(): void + { + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php new file mode 100644 index 0000000000000..f0a63b10b2a5b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php @@ -0,0 +1,612 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Bundle\Model\Selection; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for orders with bundle product + */ +class RetrieveOrdersWithBundleProductByOrderNumberTest extends GraphQlAbstract +{ + /** @var OrderRepositoryInterface */ + private $orderRepository; + + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + protected function setUp():void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthenticationHeader = $objectManager->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * Test customer order details with bundle product with child items + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + */ + public function testGetCustomerOrderBundleProduct() + { + $qty = 1; + $bundleSku = 'bundle-product-two-dropdown-options'; + $optionsAndSelectionData = $this->getBundleOptionAndSelectionData($bundleSku); + + $cartId = $this->createEmptyCart(); + $this->addBundleProductQuery($cartId, $qty, $bundleSku, $optionsAndSelectionData); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQueryBundleProduct($orderNumber); + + $customerOrderItems = $customerOrderResponse[0]; + $this->assertEquals("Pending", $customerOrderItems['status']); + $bundledItemInTheOrder = $customerOrderItems['items'][0]; + $this->assertEquals( + 'bundle-product-two-dropdown-options-simple1-simple2', + $bundledItemInTheOrder['product_sku'] + ); + $priceOfBundledItemInOrder = $bundledItemInTheOrder['product_sale_price']['value']; + $this->assertEquals(15, $priceOfBundledItemInOrder); + $this->assertArrayHasKey('bundle_options', $bundledItemInTheOrder); + $bundleOptionsFromResponse = $bundledItemInTheOrder['bundle_options']; + $this->assertNotEmpty($bundleOptionsFromResponse); + $this->assertEquals(2, count($bundleOptionsFromResponse)); + $expectedBundleOptions = + [ + [ '__typename' => 'ItemSelectedBundleOption', + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_sku' => 'simple1', + 'product_name' => 'Simple Product1', + 'quantity'=> 1, + 'price' => [ + 'value' => 1, + 'currency' => 'USD' + ] + ] + ] + ], + [ '__typename' => 'ItemSelectedBundleOption', + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_sku' => 'simple2', + 'product_name' => 'Simple Product2', + 'quantity'=> 2, + 'price' => [ + 'value' => 2, + 'currency' => 'USD' + ] + ] + ] + ], + ]; + $this->assertEquals($expectedBundleOptions, $bundleOptionsFromResponse); + $this->deleteOrder(); + } + + /** + * Test customer order details with bundle products + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testGetCustomerOrderBundleProductWithTaxesAndDiscounts() + { + $qty = 4; + $bundleSku = 'bundle-product-two-dropdown-options'; + $optionsAndSelectionData = $this->getBundleOptionAndSelectionData($bundleSku); + + $cartId = $this->createEmptyCart(); + $this->addBundleProductQuery($cartId, $qty, $bundleSku, $optionsAndSelectionData); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQueryBundleProduct($orderNumber); + + $customerOrderItems = $customerOrderResponse[0]; + $this->assertEquals("Pending", $customerOrderItems['status']); + + $bundledItemInTheOrder = $customerOrderItems['items'][0]; + $this->assertEquals( + 'bundle-product-two-dropdown-options-simple1-simple2', + $bundledItemInTheOrder['product_sku'] + ); + $this->assertEquals(6, $bundledItemInTheOrder['discounts'][0]['amount']['value']); + $this->assertEquals( + 'Discount Label for 10% off', + $bundledItemInTheOrder["discounts"][0]['label'] + ); + $this->assertArrayHasKey('bundle_options', $bundledItemInTheOrder); + $childItemsInTheOrder = $bundledItemInTheOrder['bundle_options']; + $this->assertNotEmpty($childItemsInTheOrder); + $this->assertCount(2, $childItemsInTheOrder); + $this->assertEquals('Drop Down Option 1', $childItemsInTheOrder[0]['label']); + $this->assertEquals('Drop Down Option 2', $childItemsInTheOrder[1]['label']); + + $this->assertEquals('simple1', $childItemsInTheOrder[0]['values'][0]['product_sku']); + $this->assertEquals('simple2', $childItemsInTheOrder[1]['values'][0]['product_sku']); + $this->assertTotalsOnBundleProductWithTaxesAndDiscounts($customerOrderItems['total']); + $this->deleteOrder(); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsOnBundleProductWithTaxesAndDiscounts(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(5.4, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 77.4, 'currency' =>'USD'], + 'grand_total' => ['value' => 77.4, 'currency' =>'USD'], + 'subtotal' => ['value' => 60, 'currency' =>'USD'], + 'total_tax' => ['value' => 5.4, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 21.5], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2], + 'label' => 'Discount Label for 10% off' + ] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 8, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['createEmptyCart']; + } + + /** + * Add bundle product to cart with Graphql query + * + * @param string $cartId + * @param float $qty + * @param string $sku + * @param array $optionsAndSelectionData + * @throws AuthenticationException + */ + public function addBundleProductQuery( + string $cartId, + float $qty, + string $sku, + array $optionsAndSelectionData + ) { + $query = <<<QUERY +mutation { + addBundleProductsToCart(input:{ + cart_id:"{$cartId}" + cart_items:[ + { + data:{ + sku:"{$sku}" + quantity:$qty + } + bundle_options:[ + { + id:$optionsAndSelectionData[0] + quantity:1 + value:["{$optionsAndSelectionData[1]}"] + } + { + id:$optionsAndSelectionData[2] + quantity:2 + value:["{$optionsAndSelectionData[3]}"] + } + ] + } + ] + }) { + cart { + items {quantity product {sku}} + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('cart', $response['addBundleProductsToCart']); + } + /** + * @param string $cartId + * @param array $auth + * @return array + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "John" + lastname: "Smith" + company: "Test company" + street: ["test street 1", "test street 2"] + city: "Texas City" + postcode: "78717" + telephone: "5123456677" + region: "TX" + country_code: "US" + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test shipFirst" + lastname: "test shipLast" + company: "test company" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36013" + country_code: "US" + telephone: "3347665522" + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount {value} + } + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + return $availableShippingMethod; + } + /** + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + return $availablePaymentMethod; + } + + /** + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart {selected_payment_method {code}} + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * @param string $cartId + * @return string + */ + private function placeOrder(string $cartId): string + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_number + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['placeOrder']['order']['order_number']; + } + + /** + * Get customer order query for bundle order items + * + * @param $orderNumber + * @return mixed + * @throws AuthenticationException + */ + private function getCustomerOrderQueryBundleProduct($orderNumber) + { + $query = + <<<QUERY +{ + customer { + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items { + id + number + order_date + status + items{ + __typename + product_sku + product_name + product_url_key + product_sale_price{value} + quantity_ordered + discounts{amount{value} label} + ... on BundleOrderItem{ + bundle_options{ + __typename + label + values { + product_sku + product_name + quantity + price { + value + currency + } + } + } + } + } + total { + base_grand_total{value currency} + grand_total{value currency} + subtotal {value currency } + total_tax{value currency} + taxes {amount{value currency} title rate} + total_shipping{value currency} + shipping_handling + { + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value} + discounts{amount{value} label} + taxes {amount{value} title rate} + } + discounts {amount{value currency} label} + } + } + } + } + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $customerOrderItemsInResponse = $response['customer']['orders']['items']; + return $customerOrderItemsInResponse; + } + + /** + * @return void + */ + private function deleteOrder(): void + { + /** @var \Magento\Framework\Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(Collection::class); + //$orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * @param string $bundleSku + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getBundleOptionAndSelectionData($bundleSku): array + { + /** @var Product $bundleProduct */ + $bundleProduct = $this->productRepository->get($bundleSku); + /** @var $typeInstance \Magento\Bundle\Model\Product\Type */ + $typeInstance = $bundleProduct->getTypeInstance(); + $optionsAndSelections = []; + /** @var $option \Magento\Bundle\Model\Option */ + $option1 = $typeInstance->getOptionsCollection($bundleProduct)->getFirstItem(); + $option2 = $typeInstance->getOptionsCollection($bundleProduct)->getLastItem(); + $optionId1 =(int) $option1->getId(); + $optionId2 =(int) $option2->getId(); + /** @var Selection $selection */ + $selection1 = $typeInstance->getSelectionsCollection([$option1->getId()], $bundleProduct)->getFirstItem(); + $selectionId1 = (int)$selection1->getSelectionId(); + $selection2 = $typeInstance->getSelectionsCollection([$option2->getId()], $bundleProduct)->getLastItem(); + $selectionId2 = (int)$selection2->getSelectionId(); + array_push($optionsAndSelections, $optionId1, $selectionId1, $optionId2, $selectionId2); + return $optionsAndSelections; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php index 48619d1392309..e7af77f23fa88 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -91,6 +91,6 @@ public function testGetStoreConfig() $response['storeConfig']['secure_base_static_url'] ); $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $response['storeConfig']['secure_base_media_url']); - $this->assertEquals('Test Store', $response['storeConfig']['store_name']); + $this->assertEquals($store->getName(), $response['storeConfig']['store_name']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php new file mode 100644 index 0000000000000..c0199e8908d0e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php @@ -0,0 +1,176 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a bundle product to wishlist + */ +class AddBundleProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var mixed + */ + private $productRepository; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $objectManager->get(WishlistFactory::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * + * @throws Exception + */ + public function testAddBundleProductWithOptions(): void + { + $sku = 'bundle-product'; + $product = $this->productRepository->get($sku); + $customerId = 1; + $qty = 2; + $optionQty = 1; + + /** @var Type $typeInstance */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var Option $option */ + $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); + /** @var Product $selection */ + $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); + $optionId = $option->getId(); + $selectionId = $selection->getSelectionId(); + $bundleOptions = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, $optionQty); + + $query = $this->getQuery($sku, $qty, $bundleOptions); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true); + /** @var Item $item */ + $item = $wishlist->getItemCollection()->getFirstItem(); + + $this->assertArrayHasKey('addProductsToWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $response = $response['addProductsToWishlist']['wishlist']; + $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); + $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); + $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); + $this->assertEquals($item->getData('qty'), $response['items'][0]['qty']); + $this->assertEquals($item->getDescription(), $response['items'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items'][0]['added_at']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param string $sku + * @param int $qty + * @param string $bundleOptions + * @param int $wishlistId + * + * @return string + */ + private function getQuery( + string $sku, + int $qty, + string $bundleOptions, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + selected_options: [ + "{$bundleOptions}" + ] + } + ] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * @param int $optionId + * @param int $selectionId + * + * @param int $quantity + * + * @return string + */ + private function generateBundleOptionIdV2(int $optionId, int $selectionId, int $quantity): string + { + return base64_encode("bundle/$optionId/$selectionId/$quantity"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php new file mode 100644 index 0000000000000..386df99f0d211 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a configurable product to wishlist + */ +class AddConfigurableProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $objectManager->get(WishlistFactory::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * + * @throws Exception + */ + public function testAddDownloadableProductWithOptions(): void + { + $product = $this->getConfigurableProductInfo(); + $customerId = 1; + $qty = 2; + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; + $childSku = $product['variants'][0]['product']['sku']; + $parentSku = $product['sku']; + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesIdV2Query($attributeId, $valueIndex); + + $query = $this->getQuery($parentSku, $childSku, $qty, $selectedConfigurableOptionsQuery); + + $response = $this->graphQlMutation($query, [], '', $this->getHeadersMap()); + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true); + /** @var Item $wishlistItem */ + $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); + + self::assertArrayHasKey('addProductsToWishlist', $response); + self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $wishlistResponse = $response['addProductsToWishlist']['wishlist']; + self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); + self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); + self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); + self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']); + self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']); + self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']); + self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeadersMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param string $parentSku + * @param string $childSku + * @param int $qty + * @param string $customizableOptions + * @param int $wishlistId + * + * @return string + */ + private function getQuery( + string $parentSku, + string $childSku, + int $qty, + string $customizableOptions, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$childSku}" + parent_sku: "{$parentSku}" + quantity: {$qty} + {$customizableOptions} + } + ] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * Generates Id_v2 for super configurable product super attributes + * + * @param int $attributeId + * @param int $valueIndex + * + * @return string + */ + private function generateSuperAttributesIdV2Query(int $attributeId, int $valueIndex): string + { + return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; + } + + /** + * Returns information about testable configurable product retrieved from GraphQl query + * + * @return array + * + * @throws Exception + */ + private function getConfigurableProductInfo(): array + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); + + return current($searchResponse['products']['items']); + } + + /** + * Returns GraphQl query for fetching configurable product information + * + * @param string $term + * + * @return string + */ + private function getFetchProductQuery(string $term): string + { + return <<<QUERY +{ + products( + search:"{$term}" + pageSize:1 + ) { + items { + sku + ... on ConfigurableProduct { + variants { + product { + sku + } + } + configurable_options { + attribute_id + attribute_code + id + label + position + product_id + use_default + values { + default_label + label + store_label + use_default_value + value_index + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php new file mode 100644 index 0000000000000..389f4eae4c574 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -0,0 +1,216 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a downloadable product to wishlist + */ +class AddDownloadableProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var GetCustomOptionsWithIDV2ForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * Set Up + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $this->objectManager->get(WishlistFactory::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = + $this->objectManager->get(GetCustomOptionsWithIDV2ForQueryBySku::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + */ + public function testAddDownloadableProductWithOptions(): void + { + $customerId = 1; + $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 2; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + $query = $this->getQuery($qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create(); + $wishlist->loadByCustomerId($customerId, true); + /** @var Item $wishlistItem */ + $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); + + self::assertArrayHasKey('addProductsToWishlist', $response); + self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $wishlistResponse = $response['addProductsToWishlist']['wishlist']; + self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); + self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); + self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); + self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']); + self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']); + self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']); + self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + */ + public function testAddDownloadableProductOnDisabledWishlist(): void + { + $qty = 2; + $sku = 'downloadable-product-with-purchased-separately-links'; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + $productOptionsQuery = trim(preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ), '{}'); + $query = $this->getQuery($qty, $sku, $productOptionsQuery); + $this->expectExceptionMessage('The wishlist is not currently available.'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Function returns array of all product's links + * + * @param string $sku + * + * @return array + */ + private function getProductsLinks(string $sku): array + { + $result = []; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($sku, false, null, true); + + foreach ($product->getDownloadableLinks() as $linkObject) { + $result[$linkObject->getLinkId()] = [ + 'title' => $linkObject->getTitle(), + 'price' => $linkObject->getPrice(), + ]; + } + + return $result; + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * + * @return string + */ + private function getQuery( + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: 0, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * Generates Id_v2 for downloadable links + * + * @param int $linkId + * + * @return string + */ + private function generateProductLinkSelectedOptions(int $linkId): string + { + return base64_encode("downloadable/$linkId"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php index 2208f904320d9..0a8e1757a2ce2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -32,6 +32,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php */ public function testCustomerWishlist(): void @@ -74,6 +75,7 @@ public function testCustomerWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Customer/_files/customer.php */ public function testCustomerAlwaysHasWishlist(): void @@ -100,6 +102,7 @@ public function testCustomerAlwaysHasWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 */ public function testGuestCannotGetWishlist() { @@ -121,6 +124,35 @@ public function testGuestCannotGetWishlist() $this->graphQlQuery($query); } + /** + * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCustomerCannotGetWishlistWhenDisabled() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The wishlist is not currently available.'); + + $query = + <<<QUERY +{ + customer { + wishlist { + items_count + sharing_code + updated_at + } + } +} +QUERY; + $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + } + /** * @param string $email * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php new file mode 100644 index 0000000000000..2e203e3ff4228 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.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\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for deleting a product from wishlist + */ +class DeleteProductsFromWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testDeleteWishlistItemFromWishlist(): void + { + $wishlist = $this->getWishlist(); + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlist = $wishlist['customer']['wishlist']; + $wishlistItems = $wishlist['items']; + self::assertEquals(1, $wishlist['items_count']); + + $query = $this->getQuery((int) $wishlistId, (int) $wishlistItems[0]['id']); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('removeProductsFromWishlist', $response); + self::assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']); + $wishlistResponse = $response['removeProductsFromWishlist']['wishlist']; + self::assertEquals(0, $wishlistResponse['items_count']); + self::assertEmpty($wishlistResponse['items']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * + * @return string + */ + private function getQuery( + int $wishlistId, + int $wishlistItemId + ): string { + return <<<MUTATION +mutation { + removeProductsFromWishlist( + wishlistId: {$wishlistId}, + wishlistItemsIds: [{$wishlistItemId}] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + items { + id + description + qty + } + } + } +} +MUTATION; + } + + /** + * Get wishlist result + * + * @return array + * + * @throws Exception + */ + public function getWishlist(): array + { + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + } + + /** + * Get customer wishlist query + * + * @return string + */ + private function getCustomerWishlistQuery(): string + { + return <<<QUERY +query { + customer { + wishlist { + id + items_count + items { + id + qty + description + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php new file mode 100644 index 0000000000000..6d54d9f0b4444 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; + +/** + * Generate an array with test values for customizable options with encoded id_v2 value + */ +class GetCustomOptionsWithIDV2ForQueryBySku +{ + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionRepository; + + /** + * @param ProductCustomOptionRepositoryInterface $productCustomOptionRepository + */ + public function __construct(ProductCustomOptionRepositoryInterface $productCustomOptionRepository) + { + $this->productCustomOptionRepository = $productCustomOptionRepository; + } + + /** + * Returns array of custom options for the product + * + * @param string $sku + * + * @return array + */ + public function execute(string $sku): array + { + $customOptions = $this->productCustomOptionRepository->getList($sku); + $selectedOptions = []; + $enteredOptions = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + + if ($optionType === 'field' || $optionType === 'area' || $optionType === 'date') { + $enteredOptions[] = [ + 'id' => $this->encodeEnteredOption((int)$customOption->getOptionId()), + 'value' => '2012-12-12' + ]; + } elseif ($optionType === 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $selectedOptions[] = $this->encodeSelectedOption( + (int)$customOption->getOptionId(), + (int)reset($optionSelectValues)->getOptionTypeId() + ); + } elseif ($optionType === 'multiple') { + foreach ($customOption->getValues() as $optionValue) { + $selectedOptions[] = $this->encodeSelectedOption( + (int)$customOption->getOptionId(), + (int)$optionValue->getOptionTypeId() + ); + } + } + } + + return [ + 'selected_options' => $selectedOptions, + 'entered_options' => $enteredOptions + ]; + } + + /** + * Returns id_v2 of the selected custom option + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function encodeSelectedOption(int $optionId, int $optionValueId): string + { + return base64_encode("custom-option/$optionId/$optionValueId"); + } + + /** + * Returns id_v2 of the entered custom option + * + * @param int $optionId + * + * @return string + */ + private function encodeEnteredOption(int $optionId): string + { + return base64_encode("custom-option/$optionId"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php new file mode 100644 index 0000000000000..9e96bdc5d7079 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for updating a product from wishlist + */ +class UpdateProductsFromWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testUpdateSimpleProductFromWishlist(): void + { + $wishlist = $this->getWishlist(); + $qty = 5; + $description = 'New Description'; + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + self::assertNotEquals($description, $wishlistItem['description']); + self::assertNotEquals($qty, $wishlistItem['qty']); + + $query = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('updateProductsInWishlist', $response); + self::assertArrayHasKey('wishlist', $response['updateProductsInWishlist']); + $wishlistResponse = $response['updateProductsInWishlist']['wishlist']; + self::assertEquals($qty, $wishlistResponse['items'][0]['qty']); + self::assertEquals($description, $wishlistResponse['items'][0]['description']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * @param int $qty + * @param string $description + * + * @return string + */ + private function getQuery( + int $wishlistId, + int $wishlistItemId, + int $qty, + string $description + ): string { + return <<<MUTATION +mutation { + updateProductsInWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + wishlist_item_id: "{$wishlistItemId}" + quantity: {$qty} + description: "{$description}" + } + ] +) { + userInputErrors { + code + message + } + wishlist { + id + sharing_code + items_count + items { + id + description + qty + } + } + } +} +MUTATION; + } + + /** + * Get wishlist result + * + * @return array + * + * @throws Exception + */ + public function getWishlist(): array + { + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + } + + /** + * Get customer wishlist query + * + * @return string + */ + private function getCustomerWishlistQuery(): string + { + return <<<QUERY +query { + customer { + wishlist { + id + items_count + items { + id + qty + description + } + } + } +} +QUERY; + } +} 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 bb353938239bc..88c59d6dd8428 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php @@ -39,6 +39,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php */ public function testGetCustomerWishlist(): void @@ -94,6 +95,7 @@ public function testGetCustomerWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 */ public function testGetGuestWishlist() { diff --git a/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php index 91a044f189b4c..0e277ac942263 100644 --- a/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php @@ -7,7 +7,7 @@ namespace Magento\Integration\Model; use Magento\Customer\Api\AccountManagementInterface; -use Magento\Framework\Exception\InputException; +use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Model\Oauth\Token as TokenModel; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -76,9 +76,15 @@ protected function setUp(): void } /** + * Create customer access token + * + * @dataProvider storesDataProvider * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @param string|null $store + * @return void */ - public function testCreateCustomerAccessToken() + public function testCreateCustomerAccessToken(?string $store): void { $userName = 'customer@example.com'; $password = 'password'; @@ -86,15 +92,28 @@ public function testCreateCustomerAccessToken() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $userName, 'password' => $password]; - $accessToken = $this->_webApiCall($serviceInfo, $requestData); + $accessToken = $this->_webApiCall($serviceInfo, $requestData, null, $store); $this->assertToken($accessToken, $userName, $password); } + /** + * DataProvider for testCreateCustomerAccessToken + * + * @return array + */ + public function storesDataProvider(): array + { + return [ + 'default store' => [null], + 'all store view' => ['all'], + ]; + } + /** * @dataProvider validationDataProvider */ @@ -105,7 +124,7 @@ public function testCreateCustomerAccessTokenEmptyOrNullCredentials($username, $ $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $username, 'password' => $password]; @@ -128,7 +147,7 @@ public function testCreateCustomerAccessTokenInvalidCustomer() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $customerUserName, 'password' => $password]; @@ -195,7 +214,7 @@ public function testThrottlingMaxAttempts() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $invalidCredentials = [ @@ -238,7 +257,7 @@ public function testThrottlingAccountLockout() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $invalidCredentials = [ diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php new file mode 100644 index 0000000000000..dc59a571aa136 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Quote\Api; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Quote\Api\Data\AddressInterface; + +class GuestShipmentEstimationWithExtensionAttributesTest extends WebapiAbstract +{ + const SERVICE_VERSION = 'V1'; + const SERVICE_NAME = 'quoteGuestShipmentEstimationV1'; + const RESOURCE_PATH = '/V1/guest-carts/'; + + /** + * @var ObjectManager + */ + private $objectManager; + + protected function setUp(): void + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_free_shipping.php + * @magentoApiDataFixture Magento/Sales/_files/quote.php + */ + public function testEstimateByExtendedAddress(): void + { + /** @var \Magento\Quote\Model\Quote $quote */ + $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + $quote->load('test01', 'reserved_order_id'); + $cartId = $quote->getId(); + if (!$cartId) { + $this->fail('quote fixture failed'); + } + + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cartId, 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/guest-carts/' . $cartId . '/estimate-shipping-methods', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_NAME . 'EstimateByExtendedAddress', + ], + ]; + if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { + /** @var \Magento\Quote\Model\Quote\Address $address */ + $address = $quote->getShippingAddress(); + + $data = [ + AddressInterface::KEY_ID => (int)$address->getId(), + AddressInterface::KEY_REGION => $address->getRegion(), + AddressInterface::KEY_REGION_ID => $address->getRegionId(), + AddressInterface::KEY_REGION_CODE => $address->getRegionCode(), + AddressInterface::KEY_COUNTRY_ID => $address->getCountryId(), + AddressInterface::KEY_STREET => $address->getStreet(), + AddressInterface::KEY_COMPANY => $address->getCompany(), + AddressInterface::KEY_TELEPHONE => $address->getTelephone(), + AddressInterface::KEY_POSTCODE => $address->getPostcode(), + AddressInterface::KEY_CITY => $address->getCity(), + AddressInterface::KEY_FIRSTNAME => $address->getFirstname(), + AddressInterface::KEY_LASTNAME => $address->getLastname(), + AddressInterface::KEY_CUSTOMER_ID => $address->getCustomerId(), + AddressInterface::KEY_EMAIL => $address->getEmail(), + AddressInterface::SAME_AS_BILLING => $address->getSameAsBilling(), + AddressInterface::CUSTOMER_ADDRESS_ID => $address->getCustomerAddressId(), + AddressInterface::SAVE_IN_ADDRESS_BOOK => $address->getSaveInAddressBook(), + ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => [ + 'discounts' => [] + ] + ]; + + $requestData = [ + 'cartId' => $cartId, + 'address' => $data + ]; + } else { + + $requestData = [ + 'address' => [ + 'country_id' => "US", + 'postcode' => null, + 'region' => null, + 'region_id' => null, + 'extension_attributes' => [ + 'discounts' => [] + ] + ] + ]; + } + + // Cart must be anonymous (see fixture) + $this->assertEmpty($quote->getCustomerId()); + + $result = $this->_webApiCall($serviceInfo, $requestData); + + $this->assertNotEmpty($result); + $this->assertEquals(1, count($result)); + foreach ($result as $rate) { + $this->assertEquals("flatrate", $rate['carrier_code']); + $this->assertEquals(0, $rate['amount']); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php index e5df8c18cda0c..e7ee1acda7982 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php @@ -4,27 +4,64 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Service\V1; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Test for hold order. + */ class OrderHoldTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; + private const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'salesOrderManagementV1'; + private const SERVICE_NAME = 'salesOrderManagementV1'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + } /** - * @magentoApiDataFixture Magento/Sales/_files/order.php + * Test hold order and check order items product options after. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_two_configurable_variations.php + * + * @return void */ - public function testOrderHold() + public function testOrderHold(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $order = $objectManager->get(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); + $order = $this->objectManager->get(Order::class) + ->loadByIncrementId('100000001'); + $orderId = $order->getId(); + $orderItemsProductOptions = $this->getOrderItemsProductOptions($order); + $serviceInfo = [ 'rest' => [ - 'resourcePath' => '/V1/orders/' . $order->getId() . '/hold', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'resourcePath' => '/V1/orders/' . $orderId . '/hold', + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -32,8 +69,29 @@ public function testOrderHold() 'operation' => self::SERVICE_NAME . 'hold', ], ]; - $requestData = ['id' => $order->getId()]; + $requestData = ['id' => $orderId]; $result = $this->_webApiCall($serviceInfo, $requestData); $this->assertTrue($result); + + $this->assertEquals( + $orderItemsProductOptions, + $this->getOrderItemsProductOptions($this->orderRepository->get($orderId)) + ); + } + + /** + * Return order items product options + * + * @param OrderInterface $order + * @return array + */ + private function getOrderItemsProductOptions(OrderInterface $order): array + { + $result = []; + foreach ($order->getItems() as $orderItem) { + $result[] = $orderItem->getProductOptions(); + } + + return $result; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php b/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php index 6c8d3f90cf65c..8a68e24c8a21c 100644 --- a/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php @@ -24,19 +24,24 @@ class SearchTest extends WebapiAbstract */ private $product; + /** + * @inheritDoc + */ protected function setUp(): void { $productSku = 'simple'; $objectManager = Bootstrap::getObjectManager(); - $productRepository = $objectManager->create(ProductRepositoryInterface::class); + $productRepository = $objectManager->get(ProductRepositoryInterface::class); $this->product = $productRepository->get($productSku); } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * Tests that webapi call returns response when search criteria is valid. + * + * @magentoApiDataFixture Magento/Catalog/_files/products.php */ - public function testExistingProductSearch() + public function testExistingProductSearch(): void { $productName = $this->product->getName(); @@ -47,14 +52,16 @@ public function testExistingProductSearch() self::assertArrayHasKey('search_criteria', $response); self::assertArrayHasKey('items', $response); - self::assertGreaterThan(0, count($response['items'])); + self::assertGreaterThan(1, count($response['items'])); self::assertGreaterThan(0, $response['items'][0]['id']); } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * Tests that response is empty if invalid data is provided. + * + * @magentoApiDataFixture Magento/Catalog/_files/products.php */ - public function testNonExistentProductSearch() + public function testNonExistentProductSearch(): void { $searchCriteria = $this->buildSearchCriteria('nonExistentProduct'); $serviceInfo = $this->buildServiceInfo($searchCriteria); diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php new file mode 100644 index 0000000000000..326ec789da45a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class FixturesAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php new file mode 100644 index 0000000000000..e0049895577cc --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +/** + * Test interface for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface FixturesInterface +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php new file mode 100644 index 0000000000000..ca811c222132e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php @@ -0,0 +1,229 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that fixtures override config inherited from abstract class and interface. + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class FixturesTest extends FixturesAbstractClass implements FixturesInterface +{ + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var ConfigStorage + */ + private $configStorage; + + /** + * @var FixtureCallStorage + */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * @magentoConfigFixture default_store test_section/test_group/field_2 new_value + * @magentoConfigFixture default_store test_section/test_group/field_3 new_value + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * @dataProvider interfaceDataProvider + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testInterfaceInheritance( + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @magentoConfigFixture default_store test_section/test_group/field_2 new_value + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @dataProvider abstractDataProvider + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testAbstractInheritance( + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @return array + */ + public function interfaceDataProvider(): array + { + return [ + 'first_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => 'overridden config fixture value for method', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_3' => [ + 'value' => 'new_value', + 'exists_in_db' => true, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 1, + 'fixture3_first_module.php' => 1, + ], + ], + 'second_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => 'overridden config fixture value for method', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_3' => [ + 'value' => '3rd field website scope default value', + 'exists_in_db' => false, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 1, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * @return array + */ + public function abstractDataProvider(): array + { + return [ + 'first_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => '2nd field default value', + 'exists_in_db' => false, + ], + 'test_section/test_group/field_3' => [ + 'value' => 'overridden config fixture value for data set from abstract', + 'exists_in_db' => true, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 0, + 'fixture3_first_module.php' => 1, + ], + ], + 'second_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for data set from abstract', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => '2nd field default value', + 'exists_in_db' => false, + ], + 'test_section/test_group/field_3' => [ + 'value' => '3rd field website scope default value', + 'exists_in_db' => false, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 0, + 'fixture2_first_module.php' => 0, + 'fixture1_second_module.php' => 1, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * Asserts config field values. + * + * @param array $configs + * @param string $scope + * @return void + */ + private function assertConfigFieldValues( + array $configs, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ): void { + foreach ($configs as $path => $expected) { + $this->assertEquals($expected['value'], $this->config->getValue($path, $scope, 'default')); + if ($expected['exists_in_db']) { + $this->assertEquals( + $expected['value'], + $this->configStorage->getValueFromDb($path, ScopeInterface::SCOPE_STORES, 'default') + ); + } else { + $this->assertFalse( + $this->configStorage->checkIsRecordExist($path, ScopeInterface::SCOPE_STORES, 'default') + ); + } + } + } + + /** + * Asserts count of used fixtures. + * + * @param array $fixtures + * @return void + */ + private function assertUsedFixturesCount(array $fixtures): void + { + foreach ($fixtures as $fixture => $count) { + $this->assertEquals($count, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php new file mode 100644 index 0000000000000..445aa0c501c0a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class SkipAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php new file mode 100644 index 0000000000000..99a9332460211 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Test interface for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface SkipInterface +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php new file mode 100644 index 0000000000000..e5eb1e3a419f7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Class checks that test method can be skipped using inherited from abstract class/interface override config + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class SkipTest extends SkipAbstractClass implements SkipInterface +{ + /** + * @return void + */ + public function testAbstractSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from abstract class'); + } + + /** + * @return void + */ + public function testInterfaceSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from interface'); + } + + /** + * @dataProvider skipDataProvider + * + * @param string $message + * @return void + */ + public function testSkipDataSet(string $message): void + { + $this->fail($message); + } + + /** + * @return array + */ + public function skipDataProvider(): array + { + return [ + 'first_data_set' => ['This test should be skipped in data set node inherited from abstract class'], + 'second_data_set' => ['This test should be skipped in data set node inherited from interface'], + ]; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml index 8c0badac4b1d1..c0873b9968132 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml @@ -12,6 +12,8 @@ <field id="field_1" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> <field id="field_2" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> <field id="field_3" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + <field id="field_4" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + <field id="field_5" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> </group> </section> </system> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml index 3b2f2a1ddde1e..8604428274194 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml @@ -12,6 +12,8 @@ <field_1>1st field default value</field_1> <field_2>2nd field default value</field_2> <field_3>3rd field default value</field_3> + <field_4>4th field default value</field_4> + <field_5>5th field default value</field_5> </test_group> </test_section> </default> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml index 45bc6115e3704..aadddfcd2827a 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml @@ -204,4 +204,9 @@ <dataSet name="first_data_set" skip="true"/> </method> </test> + <global> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php" /> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_4" value="4th field globally overridden value"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_5" newValue="5th field globally replaced value"/> + </global> </overrides> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml index bbb3fad88e5cd..45c45a79eeafa 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml @@ -53,4 +53,58 @@ </dataSet> </method> </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesInterface"> + <magentoAdminConfigFixture path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <method name="testInterfaceInheritance"> + <magentoAdminConfigFixture path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="second_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_3" remove="true"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_3" remove="true"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesAbstractClass"> + <method name="testAbstractInheritance"> + <magentoAdminConfigFixture path="test_section/test_group/field_2" remove="true"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_2" remove="true"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <dataSet name="first_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + </dataSet> + <dataSet name="second_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipAbstractClass"> + <method name="testAbstractSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="first_data_set" skip="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipInterface"> + <method name="testInterfaceSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="second_data_set" skip="true"/> + </method> + </test> </overrides> diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php index 73786707b417b..4af90d5038f36 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php @@ -79,7 +79,7 @@ public function __construct() */ protected static function _isClassCleanable(\ReflectionClass $reflectionClass) { - // do not process blacklisted classes from integration framework + // do not process skipped classes from integration framework foreach (self::$_classesToSkip as $notCleanableClass) { if ($reflectionClass->getName() == $notCleanableClass || is_subclass_of( $reflectionClass->getName(), diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php index 4a0ad01a909e3..f34eec274873d 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php @@ -15,10 +15,15 @@ use Magento\Framework\View\File\Collector\Decorator\ModuleDependency; use Magento\Framework\View\File\Collector\Decorator\ModuleOutput; use Magento\Framework\View\File\CollectorInterface; +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ConfigFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; use Magento\TestFramework\Workaround\Override\Config\Converter; use Magento\TestFramework\Workaround\Override\Config\Dom; use Magento\TestFramework\Workaround\Override\Config\FileCollector; use Magento\TestFramework\Workaround\Override\Config\FileResolver; +use Magento\TestFramework\Workaround\Override\Config\RelationsCollector; use Magento\TestFramework\Workaround\Override\Config\SchemaLocator; use Magento\TestFramework\Workaround\Override\Config\ValidationState; use PHPUnit\Framework\TestCase; @@ -31,7 +36,17 @@ class Config implements ConfigInterface { /** - * @var self + * List of allowed fixture types + */ + protected const FIXTURE_TYPES = [ + DataFixture::ANNOTATION, + DataFixtureBeforeTransaction::ANNOTATION, + ConfigFixture::ANNOTATION, + AdminConfigFixture::ANNOTATION, + ]; + + /** + * @var ConfigInterface */ private static $instance; @@ -40,6 +55,11 @@ class Config implements ConfigInterface */ private $config; + /** + * @var array + */ + private $inheritedConfig; + /** * Self instance getter. * @@ -54,6 +74,22 @@ public static function getInstance(): ConfigInterface return self::$instance; } + /** + * Get config from global node + * + * @param string|null $fixtureType + * @return array + */ + public function getGlobalConfig(?string $fixtureType = null): array + { + $result = $this->config['global'] ?? []; + if ($fixtureType) { + $result = $result[$fixtureType] ?? []; + } + + return $result; + } + /** * Self instance setter. * @@ -126,7 +162,7 @@ public function getSkipConfiguration(TestCase $test): array */ public function hasSkippedTest(string $className): bool { - $classConfig = $this->config[$className] ?? []; + $classConfig = $this->getInheritedClassConfig($className); return $this->isSkippedByConfig($classConfig); } @@ -136,12 +172,11 @@ public function hasSkippedTest(string $className): bool */ public function getClassConfig(TestCase $test, ?string $fixtureType = null): array { - $result = $this->config[$this->getOriginalClassName($test)] ?? []; - if ($fixtureType) { - $result = $result[$fixtureType] ?? []; - } + $config = $this->getInheritedClassConfig($this->getOriginalClassName($test)); - return $result; + return $fixtureType + ? $config[$fixtureType] ?? [] + : $config; } /** @@ -223,7 +258,7 @@ protected function getValidationState(): ValidationStateInterface */ protected function getConverter(): ConverterInterface { - return ObjectManager::getInstance()->create(Converter::class); + return ObjectManager::getInstance()->create(Converter::class, ['types' => $this::FIXTURE_TYPES]); } /** @@ -295,4 +330,70 @@ private function prepareSkipConfig(array $config): array 'skipMessage' => $config['skipMessage'] ?: 'Skipped according to override configurations', ]; } + + /** + * Returns class relation collector. + * + * @return RelationsCollector + */ + private function getRelationsCollector(): RelationsCollector + { + return ObjectManager::getInstance()->get(RelationsCollector::class); + } + + /** + * Returns config for test including config from parents. + * + * @param string $originalClassName + * @return array + */ + private function getInheritedClassConfig(string $originalClassName): array + { + if (empty($this->inheritedConfig[$originalClassName])) { + $classConfig = $this->config[$originalClassName] ?? []; + foreach ($this->getRelationsCollector()->getParents($originalClassName) as $parent) { + $parentConfig = $this->config[$parent] ?? []; + $classConfig = $this->mergeConfiguration($classConfig, $parentConfig); + } + $this->inheritedConfig[$originalClassName] = $classConfig; + } + + return $this->inheritedConfig[$originalClassName]; + } + + /** + * Merges test configurations. + * + * @param array $mainConfig + * @param array $parentConfig + * @return array + */ + private function mergeConfiguration(array $mainConfig, array $parentConfig): array + { + $merged = $mainConfig; + + foreach ($parentConfig as $key => &$value) { + if (is_array($value)) { + $merged[$key] = $merged[$key] ?? []; + if (in_array($key, $this::FIXTURE_TYPES, true)) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $merged[$key] = array_merge($merged[$key], $value); + } else { + $merged[$key] = $this->mergeConfiguration($merged[$key], $value); + } + } elseif ($key === 'skip') { + $merged['skip_from_config'] = $merged['skip_from_config'] ?? false; + $merged['skip'] = $merged['skip'] ?? false; + $merged['skipMessage'] = $merged['skipMessage'] ?? null; + + if (!$merged['skip_from_config'] && $parentConfig['skip_from_config']) { + $merged[$key] = $value; + $merged['skipMessage'] = $parentConfig['skipMessage']; + $merged['skip_from_config'] = $parentConfig['skip_from_config']; + } + } + } + + return $merged; + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php index 22d88279e8a9a..3f4b4687da793 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php @@ -18,12 +18,18 @@ */ class Converter implements ConverterInterface { - protected const FIXTURE_TYPES = [ - DataFixture::ANNOTATION, - DataFixtureBeforeTransaction::ANNOTATION, - ConfigFixture::ANNOTATION, - AdminConfigFixture::ANNOTATION, - ]; + /** + * @var array + */ + private $supportedFixtureTypes; + + /** + * @param array $types + */ + public function __construct(array $types = []) + { + $this->supportedFixtureTypes = $types; + } /** @var \DOMXPath */ private $xpath; @@ -34,7 +40,7 @@ class Converter implements ConverterInterface public function convert($source) { $this->xpath = new \DOMXPath($source); - $config = []; + $config = $this->getGlobalConfig($this->xpath); foreach ($this->xpath->query('//test') as $testOverride) { $className = ltrim($testOverride->getAttribute('class'), '\\'); $config[$className] = $this->getTestConfigByFixtureType($testOverride); @@ -67,6 +73,7 @@ public function convert($source) */ private function fillSkipSection(\DOMElement $node, array $config): array { + $config['skip_from_config'] = !empty($node->getAttribute('skip')); $config['skip'] = $node->getAttribute('skip') === 'true'; $config['skipMessage'] = $node->getAttribute('skipMessage') ?: null; @@ -81,7 +88,7 @@ private function fillSkipSection(\DOMElement $node, array $config): array */ private function getTestConfigByFixtureType(\DOMElement $node): array { - foreach ($this::FIXTURE_TYPES as $fixtureType) { + foreach ($this->supportedFixtureTypes as $fixtureType) { $currentTestNodePath = sprintf("//test[@class ='%s']/%s", $node->getAttribute('class'), $fixtureType); foreach ($this->xpath->query($currentTestNodePath) as $classDataFixture) { $config[$fixtureType][] = $this->fillAttributes($classDataFixture); @@ -182,4 +189,36 @@ protected function fillAdminConfigFixtureAttributes(\DOMElement $fixture): array 'remove' => $fixture->getAttribute('remove'), ]; } + /** + * Get global configurations + * + * @param \DOMXPath $xpath + * @return array + */ + private function getGlobalConfig(\DOMXPath $xpath): array + { + foreach ($xpath->query('//global') as $globalOverride) { + $config = $this->fillGlobalConfigByFixtureType($globalOverride); + } + + return $config ?? []; + } + + /** + * Fill global configurations node + * + * @param \DOMElement $node + * @return array + */ + private function fillGlobalConfigByFixtureType(\DOMElement $node): array + { + $config = []; + foreach ($this->supportedFixtureTypes as $fixtureType) { + foreach ($node->getElementsByTagName($fixtureType) as $fixture) { + $config['global'][$fixtureType][] = $this->fillAttributes($fixture); + } + } + + return $config; + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php new file mode 100644 index 0000000000000..2a17e7dba4904 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Workaround\Override\Config; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\Relations\Runtime; +use Magento\Framework\ObjectManager\RelationsInterface; +use PHPUnit\Framework\TestCase; + +/** + * Class collects test class parents and interfaces. + */ +class RelationsCollector +{ + /** + * @var RelationsInterface + */ + private $relations; + + /** + * @var array + */ + private $internalParents = []; + + /** + * Returns filtered list of parent classes and interfaces for given class name. + * + * @param string $className + * @return array + */ + public function getParents(string $className): array + { + return array_diff($this->getRelations($className), $this->getInternalParents()); + } + + /** + * Returns list of parent classes and interfaces for given class name. + * + * @param string $className + * @return array + */ + private function getRelations(string $className): array + { + $result = $this->getRelationsReader()->getParents($className); + + foreach ($result as $parent) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $result = array_merge($result, $this->getRelations($parent)); + } + + return $result; + } + + /** + * Returns class relations reader. + * + * @return RelationsInterface + */ + private function getRelationsReader(): RelationsInterface + { + if (empty($this->relations)) { + $this->relations = ObjectManager::getInstance()->create(Runtime::class); + } + + return $this->relations; + } + + /** + * Returns list of classes that should not be in list of parent classes. + * + * @return array + */ + private function getInternalParents(): array + { + if (empty($this->internalParents)) { + $this->internalParents = $this->getRelations(TestCase::class); + $this->internalParents[] = TestCase::class; + } + + return $this->internalParents; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php index 556f4e22d6f45..0f0579c49a94c 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php @@ -12,6 +12,9 @@ */ abstract class Base implements ApplierInterface { + /** @var array */ + private $globalConfig; + /** @var array */ private $classConfig; @@ -21,6 +24,27 @@ abstract class Base implements ApplierInterface /** @var array */ private $dataSetConfig; + /** + * Get global node config + * + * @return array + */ + public function getGlobalConfig(): array + { + return $this->globalConfig; + } + + /** + * Set global node config + * + * @param array $globalConfig + * @return void + */ + public function setGlobalConfig(array $globalConfig): void + { + $this->globalConfig = $globalConfig; + } + /** * Get class node config * @@ -92,6 +116,7 @@ public function setDataSetConfig(array $dataSetConfig): void protected function getPrioritizedConfig(): array { return [ + $this->getGlobalConfig(), $this->getClassConfig(), $this->getMethodConfig(), $this->getDataSetConfig(), diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php index 351ecb60ae34d..33bf1011c5b7b 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php @@ -127,7 +127,11 @@ public function requireDataFixture(string $path): void */ public function applyConfigFixtures(TestCase $test, array $fixtures, string $fixtureType): array { - return $this->getApplier($test, $fixtureType)->apply($fixtures); + $skipConfig = $this->config->getSkipConfiguration($test); + + return $skipConfig['skip'] + ? [] + : $this->getApplier($test, $fixtureType)->apply($fixtures); } /** @@ -136,10 +140,14 @@ public function applyConfigFixtures(TestCase $test, array $fixtures, string $fix public function applyDataFixtures(TestCase $test, array $fixtures, string $fixtureType): array { $result = []; - $fixtures = $this->getApplier($test, $fixtureType)->apply($fixtures); + $skipConfig = $this->config->getSkipConfiguration($test); + + if (!$skipConfig['skip']) { + $fixtures = $this->getApplier($test, $fixtureType)->apply($fixtures); - foreach ($fixtures as $fixture) { - $result[] = $this->processFixturePath($test, $fixture); + foreach ($fixtures as $fixture) { + $result[] = $this->processFixturePath($test, $fixture); + } } return $result; @@ -195,6 +203,7 @@ private function getApplier(TestCase $test, string $fixtureType): ApplierInterfa } /** @var Base $applier */ $applier = $this->appliersList[$fixtureType]; + $applier->setGlobalConfig($this->config->getGlobalConfig($fixtureType)); $applier->setClassConfig($this->config->getClassConfig($test, $fixtureType)); $applier->setMethodConfig($this->config->getMethodConfig($test, $fixtureType)); $applier->setDataSetConfig( diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd b/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd index 3e18c4bb7daca..424381b5cb2b9 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd @@ -10,6 +10,7 @@ <xs:complexType> <xs:sequence minOccurs="0" maxOccurs="unbounded"> <xs:element name="test" type="test" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="global" type="global" minOccurs="0" maxOccurs="unbounded" /> </xs:sequence> </xs:complexType> </xs:element> @@ -77,4 +78,12 @@ <xs:attribute name="newValue" type="xs:string"/> <xs:attribute name="remove" type="xs:boolean"/> </xs:complexType> + <xs:complexType name="global"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="magentoDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixtureBeforeTransaction" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoConfigFixture" type="configFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoAdminConfigFixture" type="adminConfigFixture" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> </xs:schema> diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php index 524e6933dfe06..4a6461a32df9d 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php @@ -484,6 +484,7 @@ private function processApply(array $existingFixtures, array $config): array */ private function setConfig(array $config): void { + $this->object->setGlobalConfig([]); $this->object->setClassConfig([]); $this->object->setDataSetConfig([]); $this->object->setMethodConfig($config); diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php index 6dd5df493353a..921c78e7bd482 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php @@ -34,8 +34,11 @@ protected function setUp(): void public function testGetPrioritizedConfig(): void { $this->object = $this->getMockBuilder(DataFixture::class) - ->setMethods(['getClassConfig', 'getMethodConfig', 'getDataSetConfig']) + ->setMethods(['getGlobalConfig','getClassConfig', 'getMethodConfig', 'getDataSetConfig']) ->getMock(); + $this->object->expects($this->once()) + ->method('getGlobalConfig') + ->willReturn(['global_config']); $this->object->expects($this->once()) ->method('getClassConfig') ->willReturn(['class_config']); @@ -46,6 +49,7 @@ public function testGetPrioritizedConfig(): void ->method('getDataSetConfig') ->willReturn(['data_set_config']); $expectedResult = [ + ['global_config'], ['class_config'], ['method_config'], ['data_set_config'], @@ -271,6 +275,7 @@ private function processApply(array $existingFixtures, array $config): array */ private function setConfig(array $config): void { + $this->object->setGlobalConfig([]); $this->object->setClassConfig([]); $this->object->setDataSetConfig([]); $this->object->setMethodConfig($config); diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php index 7ef6aa94768de..4976c8098103b 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php @@ -26,6 +26,8 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @magentoDbIsolation disabled */ class MassScheduleTest extends \PHPUnit\Framework\TestCase { @@ -64,6 +66,9 @@ class MassScheduleTest extends \PHPUnit\Framework\TestCase */ private $skus = []; + /** @var string */ + private $logFilePath; + /** * @var Registry */ diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php new file mode 100644 index 0000000000000..245656f536463 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$productIds = range(10, 12, 1); +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Bundle Product With Two dropdown options') + ->setSku('bundle-product-two-dropdown-options') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setPriceView(1) + ->setPriceType(1) + ->setPrice(10.0) + ->setShipmentType(0) + ->setBundleOptionsData( + [ + // "Drop-down" option + [ + 'title' => 'Drop Down Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 0, + 'position' => 1, + 'delete' => '', + ], + [ + 'title' => 'Drop Down Option 2', + 'default_title' => 'Option 2', + 'type' => 'select', + 'required' => 0, + 'position' => 2, + 'delete' => '', + ] + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_price_value' => 1.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_price_value' => 2.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_price_value' => 1.00, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_price_value' => 2.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ] + ], + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + $link->setPrice($linkData['selection_price_value']); + if (isset($linkData['selection_can_change_qty'])) { + $link->setCanChangeQuantity($linkData['selection_can_change_qty']); + } + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$productRepository->save($product, true); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_rollback.php new file mode 100644 index 0000000000000..7088621f14c74 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('bundle-product-two-dropdown-options', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php index fa957a0bfd3f8..1da7f821bb36e 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php @@ -28,7 +28,7 @@ ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product') ->setSku('simple1') - ->setTaxClassId(0) + ->setTaxClassId(2) ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') @@ -57,7 +57,7 @@ ->setAttributeSetId($product2->getDefaultAttributeSetId()) ->setName('Simple Product2') ->setSku('simple2') - ->setTaxClassId(0) + ->setTaxClassId(2) ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php index 13437554febd3..0d2f9d63c5d7f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php @@ -49,7 +49,7 @@ class CategoryTest extends TestCase */ protected $objectManager; - /** @var CategoryRepository */ + /** @var CategoryResource */ private $categoryResource; /** @var CategoryRepositoryInterface */ @@ -355,6 +355,17 @@ public function testDeleteChildren(): void $this->assertEquals($this->_model->getId(), null); } + /** + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/categories_no_products.php + */ + public function testChildrenCountAfterDeleteParentCategory(): void + { + $this->categoryRepository->deleteByIdentifier(3); + $this->assertEquals(8, $this->categoryResource->getChildrenCount(1)); + } + /** * @magentoDataFixture Magento/Catalog/_files/category.php */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php index 51b1d4fdb7fe0..e3b5bc8d5fd0d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php @@ -15,6 +15,8 @@ /** * Test relation customization + * + * @magentoDbIsolation disabled */ class RelationTest extends \Magento\TestFramework\Indexer\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php new file mode 100644 index 0000000000000..0d8b0a825d24c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\SpecialPriceInterface; +use Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test special price storage model + */ +class SpecialPriceStorageTest extends TestCase +{ + /** + * @var SpecialPriceStorage + */ + private $model; + /** + * @var SpecialPriceInterfaceFactory + */ + private $specialPriceFactory; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->model = $objectManager->get(SpecialPriceStorage::class); + $this->specialPriceFactory = $objectManager->get(SpecialPriceInterfaceFactory::class); + } + + /** + * Test that price update validation works correctly + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testUpdateValidationResult() + { + $date = new \Datetime('+2 days'); + $date->setTime(0, 0); + /** @var SpecialPriceInterface $price */ + $price = $this->specialPriceFactory->create(); + $price->setSku('invalid') + ->setStoreId(0) + ->setPrice(5.0) + ->setPriceFrom($date->format('Y-m-d H:i:s')) + ->setPriceTo( + $date->modify('+1 day') + ->format('Y-m-d H:i:s') + ); + $result = $this->model->update([$price]); + $this->assertCount(1, $result); + $this->assertStringContainsString( + 'The product that was requested doesn\'t exist.', + (string) $result[0]->getMessage() + ); + $price->setSku('simple333'); + $result = $this->model->update([$price]); + $this->assertCount(0, $result); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 1c709ffcacec7..72d96334e0335 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -9,6 +9,7 @@ use Magento\Eav\Api\AttributeSetRepositoryInterface; use Magento\Eav\Model\AttributeSetRepository; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\TestFramework\Eav\Model\GetAttributeGroupByName; use Magento\TestFramework\Eav\Model\ResourceModel\GetEntityIdByAttributeId; @@ -34,6 +35,9 @@ class EavTest extends AbstractEavTest */ private $setRepository; + /** @var ScopeConfigInterface */ + private $config; + /** * @inheritdoc */ @@ -43,6 +47,7 @@ protected function setUp(): void $this->attributeGroupByName = $this->objectManager->get(GetAttributeGroupByName::class); $this->getEntityIdByAttributeId = $this->objectManager->get(GetEntityIdByAttributeId::class); $this->setRepository = $this->objectManager->get(AttributeSetRepositoryInterface::class); + $this->config = $this->objectManager->get(ScopeConfigInterface::class); } /** @@ -217,4 +222,92 @@ private function prepareAttributeSet(array $additional): void $set->organizeData(array_merge($data, $additional)); $this->setRepository->save($set); } + + /** + * @magentoDataFixture Magento/Catalog/_files/attribute_page_layout_default.php + * @dataProvider testModifyMetaNewProductPageLayoutDefaultProvider + * @return void + */ + public function testModifyMetaNewProductPageLayoutDefault($attributesMeta): void + { + $defaultLayout = $this->config->getValue('web/default_layouts/default_product_layout'); + if ($defaultLayout) { + $attributesMeta = array_merge($attributesMeta, ['default' => $defaultLayout]); + } + $expectedMeta = $this->addMetaNesting( + $attributesMeta, + 'design', + 'page_layout' + ); + $this->callModifyMetaAndAssert($this->getNewProduct(), $expectedMeta); + } + + /** + * @return array + */ + public function testModifyMetaNewProductPageLayoutDefaultProvider(): array + { + return [ + 'attributes_meta' => [ + [ + 'dataType' => 'select', + 'formElement' => 'select', + 'visible' => '1', + 'required' => false, + 'label' => 'Layout', + 'code' => 'page_layout', + 'source' => 'design', + 'scopeLabel' => '[STORE VIEW]', + 'globalScope' => false, + 'sortOrder' => '__placeholder__', + 'options' => + [ + 0 => + [ + 'value' => '', + 'label' => 'No layout updates', + '__disableTmpl' => true, + ], + 1 => + [ + 'label' => 'Empty', + 'value' => 'empty', + '__disableTmpl' => true, + ], + 2 => + [ + 'label' => '1 column', + 'value' => '1column', + '__disableTmpl' => true, + ], + 3 => + [ + 'label' => '2 columns with left bar', + 'value' => '2columns-left', + '__disableTmpl' => true, + ], + 4 => + [ + 'label' => '2 columns with right bar', + 'value' => '2columns-right', + '__disableTmpl' => true, + ], + 5 => + [ + 'label' => '3 columns', + 'value' => '3columns', + '__disableTmpl' => true, + ], + ], + 'componentType' => 'field', + 'disabled' => true, + 'validation' => + [ + 'required' => false, + ], + 'serviceDisabled' => true, + ] + ] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php new file mode 100644 index 0000000000000..c8222ac565dc7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Catalog\Setup\CategorySetup; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +$attribute->loadByCode($entityType, 'page_layout'); +$attribute->setData('default_value', '1column'); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php new file mode 100644 index 0000000000000..f762574a2efd1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Catalog\Setup\CategorySetup; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +$attribute->loadByCode($entityType, 'page_layout'); +$attribute->setData('default_value', null); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php index 514c6563622c9..a7e4f702e5630 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php @@ -95,7 +95,7 @@ ->setPrice(10) ->setWeight(1) ->setShortDescription("Short description") - ->setTaxClassId(0) + ->setTaxClassId(2) ->setTierPrices($tierPrices) ->setDescription('Description with <b>html tag</b>') ->setExtensionAttributes($productExtensionAttributesWebsiteIds) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php index 4ed783100fa98..7c8ce4c63034d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php @@ -20,6 +20,9 @@ $entityTypeId = $entityModel->setType(\Magento\Catalog\Model\Product::ENTITY)->getTypeId(); $groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); +/** @var \Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple', true); + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $attribute->setAttributeCode( @@ -30,6 +33,8 @@ 'text' )->setFrontendLabel( 'custom_attributes_frontend_label' +)->setAttributeSetId( + $product->getDefaultAttributeSetId() )->setAttributeGroupId( $groupId )->setIsFilterable( @@ -40,8 +45,6 @@ $attribute->getBackendTypeByInput($attribute->getFrontendInput()) )->save(); -$product = $productRepository->get('simple', true); - $product->setCustomAttribute($attribute->getAttributeCode(), 'customAttributeValue'); $productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php index d8222d0ce5c49..0dbcb998da836 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php @@ -13,6 +13,7 @@ ->setSku('simple1') ->setPrice(10) ->setDescription('Description with <b>html tag</b>') + ->setTaxClassId(2) ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setCategoryIds([2]) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php new file mode 100644 index 0000000000000..c2ebfa4389ab2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php @@ -0,0 +1,162 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + /** @var $store \Magento\Store\Model\Store */ + $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); + $store = $store->load('test', 'code'); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => [ + Store::DEFAULT_STORE_ID => 'Option Admin Store', + Store::DISTRO_STORE_ID => 'Option Default Store', + $store->getId() => 'Option Test Store' + ], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +} + +$eavConfig->clear(); + +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(10) + ->setAttributeSetId(4) + ->setName('Simple Product1') + ->setSku('simple1') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_IN_CART) + ->setPrice(10) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('5.99') + ->save(); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(11) + ->setAttributeSetId(4) + ->setName('Simple Product2') + ->setSku('simple2') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_ON_GESTURE) + ->setPrice(20) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('15.99') + ->save(); + +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId( + 333 +)->setCreatedAt( + '2014-06-23 09:50:07' +)->setName( + 'Category 1' +)->setParentId( + 2 +)->setPath( + '1/2/333' +)->setLevel( + 2 +)->setAvailableSortBy( + ['position', 'name'] +)->setDefaultSortBy( + 'name' +)->setIsActive( + true +)->setPosition( + 1 +)->setPostedProducts( + [10 => 10, 11 => 11] +)->save(); + +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); + +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php new file mode 100644 index 0000000000000..6793051b5787b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @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 = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple1', 'simple2'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed + } +} + +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +/** @var $category \Magento\Catalog\Model\Category */ +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->load(333); +if ($category->getId()) { + $category->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index cfd07f57a4cd8..bc13fdab302af 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Export\Adapter\AbstractAdapter; use Magento\Store\Model\Store; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; /** * Abstract class for testing product export and import scenarios @@ -242,6 +244,7 @@ protected function executeImportDeleteTest(array $skus, string $csvFile = null): */ protected function executeFixtures(array $fixtures, bool $rollback = false) { + Resolver::getInstance()->setCurrentFixtureType(DataFixture::ANNOTATION); foreach ($fixtures as $fixture) { $fixturePath = $this->resolveFixturePath($fixture, $rollback); include $fixturePath; diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 4d08d71793cbb..d3f012bb0852f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -3127,4 +3127,98 @@ public function testCheckDoubleImportOfProducts() $productsAfterSecondImport = $this->productRepository->getList($searchCriteria)->getItems(); $this->assertCount(3, $productsAfterSecondImport); } + + /** + * Checks that product related links added for all bunches properly after products import + */ + public function testImportProductsWithLinksInDifferentBunches() + { + $this->importedProducts = [ + 'simple1', + 'simple2', + 'simple3', + 'simple4', + 'simple5', + 'simple6', + ]; + $importExportData = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $importExportData->expects($this->atLeastOnce()) + ->method('getBunchSize') + ->willReturn(5); + $this->_model = $this->objectManager->create( + \Magento\CatalogImportExport\Model\Import\Product::class, + ['importExportData' => $importExportData] + ); + $linksData = [ + 'related' => [ + 'simple1' => '2', + 'simple2' => '1' + ] + ]; + $pathToFile = __DIR__ . '/_files/products_to_import_with_related.csv'; + $filesystem = $this->objectManager->create(Filesystem::class); + + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + Csv::class, + [ + 'file' => $pathToFile, + 'directory' => $directory + ] + ); + $errors = $this->_model->setSource($source) + ->setParameters( + [ + 'behavior' => Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ] + ) + ->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->_model->importData(); + + $resource = $this->objectManager->get(ProductResource::class); + $productId = $resource->getIdBySku('simple6'); + /** @var Product $product */ + $product = $this->objectManager->create(Product::class); + $product->load($productId); + $productLinks = [ + 'related' => $product->getRelatedProducts() + ]; + $importedProductLinks = []; + foreach ($productLinks as $linkType => $linkedProducts) { + foreach ($linkedProducts as $linkedProductData) { + $importedProductLinks[$linkType][$linkedProductData->getSku()] = $linkedProductData->getPosition(); + } + } + $this->assertEquals($linksData, $importedProductLinks); + } + + /** + * Tests that image name does not have to be prefixed by slash + * + * @magentoDataFixture mediaImportImageFixture + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + */ + public function testUpdateImageByNameNotPrefixedWithSlash() + { + $expectedLabelForDefaultStoreView = 'image label updated'; + $expectedImageFile = '/m/a/magento_image.jpg'; + $secondStoreCode = 'fixturestore'; + $productSku = 'simple'; + $this->importDataForMediaTest('import_image_name_without_slash.csv'); + $product = $this->getProductBySku($productSku); + $imageItems = $product->getMediaGalleryImages()->getItems(); + $this->assertCount(1, $imageItems); + $imageItem = array_shift($imageItems); + $this->assertEquals($expectedImageFile, $imageItem->getFile()); + $this->assertEquals($expectedLabelForDefaultStoreView, $imageItem->getLabel()); + $product = $this->getProductBySku($productSku, $secondStoreCode); + $imageItems = $product->getMediaGalleryImages()->getItems(); + $this->assertCount(0, $imageItems); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv new file mode 100644 index 0000000000000..415501daf89d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv @@ -0,0 +1,3 @@ +"sku","store_view_code","base_image","base_image_label","hide_from_product_page" +"simple",,"m/a/magento_image.jpg","image label updated", +"simple","fixturestore",,,"m/a/magento_image.jpg" diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv new file mode 100644 index 0000000000000..3627cdc24ec41 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv @@ -0,0 +1,7 @@ +sku,product_type,store_view_code,name,price,qty,attribute_set_code,related_skus,related_position +simple1,simple,,simple 1,25,10,Default,, +simple2,simple,,simple 2,34,10,Default,, +simple3,simple,,simple 3,58,10,Default,"simple1,simple2","1,2" +simple4,simple,,simple 4,67,10,Default,"simple1,simple2","2,1" +simple5,simple,,simple 5,58,10,Default,"simple1,simple2","1,2" +simple6,simple,,simple 6,67,10,Default,"simple1,simple2","2,1" \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php index 66b452d234366..ee99ec96bbf2c 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'Checkout Agreement (active)', 'content' => 'Checkout agreement content: <b>HTML</b>', @@ -15,4 +28,4 @@ 'is_html' => true, 'stores' => [0, 1], ]); -$agreement->save(); +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php index da65dcae7d8f4..10879d3d91306 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php @@ -3,10 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Checkout Agreement (active)', 'name'); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php index e60c754d66a3c..29b01163df514 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'Checkout Agreement (inactive)', 'content' => 'Checkout agreement content: TEXT', @@ -15,4 +28,4 @@ 'is_html' => false, 'stores' => [0, 1], ]); -$agreement->save(); +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php index 39ba6cf30be26..3fda82782ebc5 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php @@ -4,10 +4,22 @@ * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Checkout Agreement (inactive)', 'name'); +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'Checkout Agreement (inactive)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php index 3be16338110a1..8d15bf6e9b74f 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'First Checkout Agreement (active)', 'content' => 'Checkout agreement content: TEXT', @@ -16,8 +29,9 @@ 'mode' => 1, 'stores' => [0, 1], ]); -$agreement->save(); -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); +$agreementResource->save($agreement); + +$agreement = $objectManager->create(Agreement::class); $agreement->setData([ 'name' => 'Second Checkout Agreement (active)', 'content' => 'Checkout agreement content: TEXT', @@ -28,4 +42,5 @@ 'mode' => 1, 'stores' => [0, 1], ]); -$agreement->save(); + +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php index 9c594c0c22b65..f43f7a5ba9a51 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php @@ -4,15 +4,28 @@ * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('First Checkout Agreement (active)', 'name'); +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'First Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Second Checkout Agreement (active)', 'name'); + +$agreement = $objectManager->create(Agreement::class); +$agreementResource->load($agreement, 'Second Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } diff --git a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php index 8aad6049125a8..660d59f3264ec 100644 --- a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php @@ -25,7 +25,7 @@ use PHPUnit\Framework\TestCase; /** - * Test for plugin which is listening store resource model and on save replace cms page url rewrites + * Test for plugin which is listening store resource model and on save replace cms page url rewrites. * * @magentoAppArea adminhtml * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -84,9 +84,10 @@ protected function setUp(): void /** * Test of replacing cms page url rewrites on create and delete store * + * @magentoDataFixture Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php * @magentoDataFixture Magento/Cms/_files/pages.php */ - public function testUrlRewritesChangesAfterStoreSave() + public function testUrlRewritesChangesAfterStoreSave(): void { $storeId = $this->createStore(); $this->assertUrlRewritesCount($storeId, 'page100', 1); @@ -98,16 +99,16 @@ public function testUrlRewritesChangesAfterStoreSave() } /** - * Assert url rewrites count by store id + * Assert url rewrites count by store id and request path * * @param int $storeId - * @param string $pageIdentifier + * @param string $requestPath * @param int $expectedCount */ - private function assertUrlRewritesCount(int $storeId, string $pageIdentifier, int $expectedCount): void + private function assertUrlRewritesCount(int $storeId, string $requestPath, int $expectedCount): void { $data = [ - UrlRewrite::REQUEST_PATH => $pageIdentifier, + UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::STORE_ID => $storeId ]; $urlRewrites = $this->urlFinder->findAllByData($data); @@ -116,8 +117,6 @@ private function assertUrlRewritesCount(int $storeId, string $pageIdentifier, in /** * Create test store - * - * @return int */ private function createStore(): int { @@ -134,7 +133,6 @@ private function createStore(): int * Delete test store * * @param int $storeId - * @return void */ private function deleteStore(int $storeId): void { diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php index 1b7a504959d54..eedb93099b8c3 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php @@ -5,12 +5,18 @@ */ namespace Magento\Config\Model; +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Config\Model\ResourceModel\Config\Data\Collection; +use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * @magentoAppArea adminhtml */ -class ConfigTest extends \PHPUnit\Framework\TestCase +class ConfigTest extends TestCase { /** * @covers \Magento\Config\Model\Config::save @@ -22,25 +28,25 @@ class ConfigTest extends \PHPUnit\Framework\TestCase public function testSaveWithSingleStoreModeEnabled($groups) { Bootstrap::getObjectManager()->get( - \Magento\Framework\Config\ScopeInterface::class + ScopeInterface::class )->setCurrentScope( - \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE + FrontNameResolver::AREA_CODE ); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->setWebsite('base')->load(); $this->assertEmpty($_configData); - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configDataObject->setSection('dev')->setGroups($groups)->save(); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->load(); $this->assertArrayHasKey('dev/debug/template_hints_admin', $_configData); $this->assertArrayHasKey('dev/debug/template_hints_blocks', $_configData); - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->setWebsite('base')->load(); $this->assertArrayNotHasKey('dev/debug/template_hints_admin', $_configData); $this->assertArrayNotHasKey('dev/debug/template_hints_blocks', $_configData); @@ -63,16 +69,16 @@ public function testSave($section, $groups, $expected) { $objectManager = Bootstrap::getObjectManager(); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = $objectManager->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = $objectManager->create(Config::class); $_configDataObject->setSection($section)->setWebsite('base')->setGroups($groups)->save(); foreach ($expected as $group => $expectedData) { - $_configDataObject = $objectManager->create(\Magento\Config\Model\Config::class); + $_configDataObject = $objectManager->create(Config::class); $_configData = $_configDataObject->setSection($group)->setWebsite('base')->load(); if (array_key_exists('payment/payflow_link/pwd', $_configData)) { $_configData['payment/payflow_link/pwd'] = $objectManager->get( - \Magento\Framework\Encryption\EncryptorInterface::class + EncryptorInterface::class )->decrypt( $_configData['payment/payflow_link/pwd'] ); @@ -85,4 +91,102 @@ public function saveDataProvider() { return require __DIR__ . '/_files/config_section.php'; } + + /** + * @param string $website + * @param string $section + * @param array $override + * @param array $inherit + * @param array $expected + * @dataProvider saveWebsiteScopeDataProvider + */ + public function testSaveUseDefault( + string $website, + string $section, + array $override, + array $inherit, + array $expected + ): void { + $objectManager = Bootstrap::getObjectManager(); + /** @var Config $config*/ + $configFactory = $objectManager->create(ConfigFactory::class); + $config = $configFactory->create() + ->setSection($section) + ->setWebsite($website) + ->setGroups($override['groups']) + ->save(); + + $paths = array_keys($expected); + + $this->assertEquals( + $expected, + $this->getConfigValues($config->getScope(), $config->getScopeId(), $paths) + ); + + $config = $configFactory->create() + ->setSection($section) + ->setWebsite($website) + ->setGroups($inherit['groups']) + ->save(); + + $this->assertEmpty( + $this->getConfigValues($config->getScope(), $config->getScopeId(), $paths) + ); + } + + /** + * @return array + */ + public function saveWebsiteScopeDataProvider(): array + { + return [ + [ + 'website' => 'base', + 'section' => 'payment', + [ + 'groups' => [ + 'account' => [ + 'fields' => [ + 'merchant_country' => ['value' => 'GB'], + ], + ], + ] + ], + [ + 'groups' => [ + 'account' => [ + 'fields' => [ + 'merchant_country' => ['inherit' => 1], + ], + ], + ], + ], + 'expected' => [ + 'paypal/general/merchant_country' => 'GB', + ], + ] + ]; + } + + /** + * @param string $scope + * @param int $scopeId + * @param array $paths + * @return array + */ + private function getConfigValues(string $scope, int $scopeId, array $paths): array + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Collection $configCollection */ + $configCollectionFactory = $objectManager->create(CollectionFactory::class); + $configCollection = $configCollectionFactory->create(); + $configCollection->addFieldToFilter('scope', $scope); + $configCollection->addFieldToFilter('scope_id', $scopeId); + $configCollection->addFieldToFilter('path', ['in' => $paths]); + $result = []; + foreach ($configCollection as $data) { + $result[$data->getPath()] = $data->getValue(); + } + return $result; + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/AttributeValuesTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/AttributeValuesTest.php new file mode 100644 index 0000000000000..b0a1c81857221 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/AttributeValuesTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps; + +use Magento\Backend\Model\Auth\Session; +use Magento\ConfigurableProduct\Block\DataProviders\PermissionsData; +use Magento\Framework\View\Layout; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class AttributeValuesTest extends TestCase +{ + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php + */ + public function testRestrictedUserNotAllowedToManageAttributes() + { + $user = Bootstrap::getObjectManager()->create( + User::class + )->loadByUsername( + 'admincatalog_user' + ); + + /** @var $session Session */ + $session = Bootstrap::getObjectManager()->get( + Session::class + ); + $session->setUser($user); + + /** @var $layout Layout */ + $layout = Bootstrap::getObjectManager()->get( + LayoutInterface::class + ); + + /** @var \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */ + $block = $layout->createBlock( + AttributeValues::class, + 'step2', + [ + 'data' => [ + 'config' => [ + 'form' => 'product_form.product_form', + 'modal' => 'configurableModal', + 'dataScope' => 'productFormConfigurable', + ], + 'permissions' => Bootstrap::getObjectManager()->get(PermissionsData::class) + ] + ] + ); + $isAllowedToManageAttributes = $block->getPermissions()->isAllowedToManageAttributes(); + $html = $block->toHtml(); + $this->assertFalse($isAllowedToManageAttributes); + $this->assertStringNotContainsString('<button class="action-create-new action-tertiary"', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php new file mode 100644 index 0000000000000..7fd64c95f9942 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Authorization\Model\Acl\Role\Group; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\UserContextInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; + +/** @var Role $role */ +$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); +$role->setName('role_catalog_permissions'); +$role->setData('role_name', $role->getName()); +$role->setRoleType(Group::ROLE_TYPE); +$role->setUserType((string)UserContextInterface::USER_TYPE_ADMIN); +$role->save(); + +/** @var $rule Rules */ +$rule = Bootstrap::getObjectManager()->create(Rules::class); +$rule->setRoleId($role->getId())->setResources(['Magento_Catalog::catalog'])->saveRel(); + +/** @var User $user */ +$user = Bootstrap::getObjectManager()->create(User::class); +$user->setData( + [ + 'firstname' => 'firstname', + 'lastname' => 'lastname', + 'email' => 'admincatalog@example.com', + 'username' => 'admincatalog_user', + 'password' => 'admincatalog_password1', + 'is_active' => 1, + ] +); +$user->setRoleId($role->getId())->save(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_rollback.php new file mode 100644 index 0000000000000..743503d1bd388 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_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\Authorization\Model\Role; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\RulesFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; + +// Deleting the user and the role. +/** @var User $user */ +$user = Bootstrap::getObjectManager()->create(User::class); +$user->loadByUsername('admincatalog_user')->delete(); +/** @var Role $role */ +$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); +$role->load('role_catalog_permissions', 'role_name'); +if ($role->getId()) { + /** @var Rules $rules */ + $rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create(); + $rules->load($role->getId(), 'role_id'); + $rules->delete(); + $role->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php index e6d3c5aa39d15..1d06aa7201f64 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php @@ -138,6 +138,22 @@ public function testFaxEnabled(): void $this->assertStringContainsString('title="Fax"', $block->toHtml()); } + /** + * @magentoDataFixture Magento/Customer/_files/attribute_city_store_label_address.php + */ + public function testCityWithStoreLabel(): void + { + /** @var \Magento\Customer\Block\Form\Register $block */ + $block = Bootstrap::getObjectManager()->create( + Register::class + )->setTemplate('Magento_Customer::form/register.phtml') + ->setShowAddressFields(true); + $this->setAttributeDataProvider($block); + + $this->assertStringNotContainsString('title="City"', $block->toHtml()); + $this->assertStringContainsString('title="Suburb"', $block->toHtml()); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php index 00b5d2bc6f279..8651db95ae645 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php @@ -19,6 +19,11 @@ use Magento\Framework\Api\SortOrder; use Magento\Framework\Config\CacheInterface; use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\InvoiceOrderInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Customer\Api\Data\AddressInterface; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -34,12 +39,18 @@ */ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const CUSTOMER_ID = 1; + /** @var AccountManagementInterface */ private $accountManagement; /** @var CustomerRepositoryInterface */ private $customerRepository; + /** @var OrderRepositoryInterface */ + private $orderRepository; + /** @var ObjectManagerInterface */ private $objectManager; @@ -71,6 +82,7 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); + $this->orderRepository = $this->objectManager->create(OrderRepositoryInterface::class); $this->customerFactory = $this->objectManager->create(CustomerInterfaceFactory::class); $this->addressFactory = $this->objectManager->create(AddressInterfaceFactory::class); $this->regionFactory = $this->objectManager->create(RegionInterfaceFactory::class); @@ -625,4 +637,55 @@ public function testUpdateDefaultShippingAndDefaultBillingTest() 'Default shipping should not be overridden' ); } + + /** + * Test that UpgradeOrderCustomerEmailObserver is executed + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoDbIsolation enabled + */ + public function testUpgradeOrderCustomerEmailObserverWhenEmailIsModified() + { + $customer = $this->customerRepository->getById(self::CUSTOMER_ID); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + + $this->customerRepository->save($customer); + + /** @var SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $searchCriteria = $searchBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + $customerOrders = $this->orderRepository->getList($searchCriteria); + + foreach ($customerOrders as $customerOrder) { + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $customerOrder->getCustomerEmail()); + } + } + + /** + * Test that UpgradeOrderCustomerEmailObserver is executed but does not update orders + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoDbIsolation enabled + */ + public function testUpgradeOrderCustomerEmailObserverWhenEmailIsNotModified(): void + { + $customer = $this->customerRepository->getById(self::CUSTOMER_ID); + + $this->customerRepository->save($customer); + + /** @var SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $searchCriteria = $searchBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + $customerOrders = $this->orderRepository->getList($searchCriteria); + + foreach ($customerOrders as $customerOrder) { + $this->assertEquals('customer@null.com', $customerOrder->getCustomerEmail()); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php new file mode 100644 index 0000000000000..8a4afc23aaea8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +//@codingStandardsIgnoreFile +/** @var \Magento\Customer\Model\Attribute $model */ +$model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Attribute::class); +/** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ +$storeManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\StoreManager::class); +$model->loadByCode('customer_address', 'city'); +$storeLabels = $model->getStoreLabels(); +$stores = $storeManager->getStores(); +/** @var \Magento\Store\Api\Data\WebsiteInterface $website */ +foreach ($stores as $store) { + $storeLabels[$store->getId()] = 'Suburb'; +} +$model->setStoreLabels($storeLabels); +$model->save(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php index 3a39e62af0ccb..9c24e4b5ff3bd 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php @@ -3,39 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//Create customer -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'CharlesTAlston@teleworm.us' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Charles' -)->setLastname( - 'Alston' -)->setGender( - '2' -); + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ +$customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(1) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('CharlesTAlston@teleworm.us') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Charles') + ->setLastname('Alston') + ->setGender('2'); + $customer->isObjectNew(true); // Create address -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); // default_billing and default_shipping information would not be saved, it is needed only for simple check $address->addData( [ @@ -54,14 +56,12 @@ // Assign customer and address $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); // Mark last address as default billing and default shipping for current customer $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); -/** @var $objectManager \Magento\TestFramework\ObjectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$objectManager->get(\Magento\Framework\Registry::class)->unregister('_fixture/Magento_ImportExport_Customer'); -$objectManager->get(\Magento\Framework\Registry::class)->register('_fixture/Magento_ImportExport_Customer', $customer); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customer'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customer', $customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php index b8a69def69d6b..46086e00244ee 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php @@ -3,41 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var $objectManager ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); + $customers = []; -//Create customer -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'BetsyParker@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Betsy' -)->setLastname( - 'Parker' -)->setGender( - 2 -); +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ +$customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(1) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('BetsyParker@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Betsy') + ->setLastname('Parker') + ->setGender(2); $customer->isObjectNew(true); // Create address -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); // default_billing and default_shipping information would not be saved, it is needed only for simple check $address->addData( [ @@ -56,46 +59,31 @@ // Assign customer and address $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); // Mark last address as default billing and default shipping for current customer $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 2 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'AnthonyNealy@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Anthony' -)->setLastname( - 'Nealy' -)->setGender( - 1 -); +$customer = $objectManager->create(Customer::class); +$customer->setWebsiteId(1) + ->setEntityId(2) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('AnthonyNealy@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Anthony') + ->setLastname('Nealy') + ->setGender(1); $customer->isObjectNew(true); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Anthony', @@ -112,7 +100,7 @@ ); $customer->addAddress($address); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Anthony', @@ -129,45 +117,30 @@ ); $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 3 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'LoriBanks@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Lori' -)->setLastname( - 'Banks' -)->setGender( - 2 -); +$customer = $objectManager->create(Customer::class); +$customer->setWebsiteId(1) + ->setEntityId(3) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('LoriBanks@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Lori') + ->setLastname('Banks') + ->setGender(2); $customer->isObjectNew(true); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Lori', @@ -183,17 +156,13 @@ ] ); $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -/** @var $objectManager \Magento\TestFramework\ObjectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$objectManager->get(\Magento\Framework\Registry::class) - ->unregister('_fixture/Magento_ImportExport_Customers_Array'); -$objectManager->get(\Magento\Framework\Registry::class) - ->register('_fixture/Magento_ImportExport_Customers_Array', $customers); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customers_Array'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customers_Array', $customers); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php index 9b989779e4cbd..302ac055f61ca 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php @@ -4,107 +4,75 @@ * See COPYING.txt for license details. */ -use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\ObjectManagerInterface; +declare(strict_types=1); + use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; /** @var $objectManager ObjectManagerInterface */ $objectManager = Bootstrap::getObjectManager(); $customers = []; + +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ $customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'customer@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Firstname' -)->setLastname( - 'Lastname' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('customer@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Firstname') + ->setLastname('Lastname') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; $customer = $objectManager->create(Customer::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'julie.worrell@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Julie' -)->setLastname( - 'Worrell' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('julie.worrell@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Julie') + ->setLastname('Worrell') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; $customer = $objectManager->create(Customer::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'david.lamar@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'David' -)->setLastname( - 'Lamar' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('david.lamar@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('David') + ->setLastname('Lamar') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$objectManager->get(Registry::class) - ->unregister('_fixture/Magento_ImportExport_Customer_Collection'); -$objectManager->get(Registry::class) - ->register('_fixture/Magento_ImportExport_Customer_Collection', $customers); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customer_Collection'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customer_Collection', $customers); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php index 9a90061a6de76..ca32958e66639 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php @@ -3,43 +3,39 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//Create customer -/** @var Magento\Customer\Model\Customer $customer */ -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 0 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'BetsyParker@example.com' -)->setPassword( - 'password' -)->setGroupId( - 0 -)->setStoreId( - 0 -)->setIsActive( - 1 -)->setFirstname( - 'Betsy' -)->setLastname( - 'Parker' -)->setGender( - 2 -); + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Customer $customer + * @var CustomerResource $customerResource + */ +$customer = Bootstrap::getObjectManager()->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(0) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('BetsyParker@example.com') + ->setPassword('password') + ->setGroupId(0) + ->setStoreId(0) + ->setIsActive(1) + ->setFirstname('Betsy') + ->setLastname('Parker') + ->setGender(2); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); -// Create and set addresses -$addressFirst = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Address::class -); +$addressFirst = $objectManager->create(Address::class); $addressFirst->addData( [ 'entity_id' => 1, @@ -57,9 +53,7 @@ $customer->addAddress($addressFirst); $customer->setDefaultBilling($addressFirst->getId()); -$addressSecond = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Address::class -); +$addressSecond = $objectManager->create(Address::class); $addressSecond->addData( [ 'entity_id' => 2, @@ -76,4 +70,4 @@ $addressSecond->isObjectNew(true); $customer->addAddress($addressSecond); $customer->setDefaultShipping($addressSecond->getId()); -$customer->save(); +$customerResource->save($customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php b/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php deleted file mode 100644 index 2ea0e58fddaba..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** @var \Magento\Customer\Model\Customer $customer */ -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -)->load( - 1 -); - -/** @var \Magento\Sales\Model\Order $order */ -$order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order::class -)->loadByIncrementId( - '100000001' -); -$order->setCustomerIsGuest(false)->setCustomerId($customer->getId())->setCustomerEmail($customer->getEmail()); -$order->save(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php index 8388f2e81c0aa..5dfab6fcc756c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php @@ -9,6 +9,9 @@ */ namespace Magento\Framework\DB\Adapter; +/** + * @magentoDbIsolation disabled + */ class InterfaceTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php index 345302a374081..6e3391bd8959f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php @@ -10,6 +10,11 @@ use Magento\Framework\DB\Ddl\Table; use Magento\TestFramework\Helper\Bootstrap; +/** + * Class checks Mysql adapter behaviour + * + * @magentoDbIsolation disabled + */ class MysqlTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php index d4507237b0ad1..db5e90d46880c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php @@ -67,10 +67,13 @@ public function testTransactionLevelDbIsolationEnabled() $this->assertEquals(1, $resourceConnection->getConnection('default')->getTransactionLevel()); } + /** + * @magentoDataFixture Magento/Framework/DB/_files/dummy_fixture.php + */ public function testTransactionLevelDbIsolationDefault() { $resourceConnection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Framework\App\ResourceConnection::class); - $this->assertEquals(0, $resourceConnection->getConnection('default')->getTransactionLevel()); + $this->assertEquals(1, $resourceConnection->getConnection('default')->getTransactionLevel()); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php b/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php new file mode 100644 index 0000000000000..2dc96aa234590 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +//this fixture should not do anything diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php index 2797cad61084c..ba2225fbe5eac 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php @@ -9,6 +9,8 @@ /** * Test Class for \Magento\Framework\Mview\View\Changelog + * + * @magentoDbIsolation disabled */ class ChangelogTest extends \PHPUnit\Framework\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php new file mode 100644 index 0000000000000..4cbe088893b03 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\GiftMessage\Model\Message; +use Magento\GiftMessage\Model\ResourceModel\Message as MessageResource; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResource; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products.php'); + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var QuoteResource $quote */ +$quote = $objectManager->create(QuoteResource::class); + +/** @var Quote $quoteModel */ +$quoteModel = $objectManager->create(Quote::class); +$quoteModel->setData(['store_id' => 1, 'is_active' => 1, 'is_multi_shipping' => 0]); +$quote->save($quoteModel); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); + +$quoteModel->setReservedOrderId('test_guest_order_with_gift_message') + ->addProduct($product, 1); +$quoteModel->collectTotals(); +$quote->save($quoteModel); + +/** @var MessageResource $message */ +$message = $objectManager->create(MessageResource::class); + +/** @var Message $message */ +$messageModel = $objectManager->create(Message::class); + +$messageModel->setSender('John Doe'); +$messageModel->setRecipient('Jane Roe'); +$messageModel->setMessage('Gift Message Text'); +$message->save($messageModel); + +$quoteModel->getItemByProduct($product)->setGiftMessageId($messageModel->getId()); +$quote->save($quoteModel); + +/** @var QuoteIdMaskResource $quoteIdMask */ +$quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); + +/** @var QuoteIdMask $quoteIdMaskModel */ +$quoteIdMaskModel = $objectManager->create(QuoteIdMask::class); + +$quoteIdMaskModel->setQuoteId($quoteModel->getId()); +$quoteIdMaskModel->setDataChanges(true); +$quoteIdMask->save($quoteIdMaskModel); diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php new file mode 100644 index 0000000000000..9c215cb432b45 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Product; +use Magento\Framework\Registry; +use Magento\GiftMessage\Model\Message; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; + +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$objectManager = Bootstrap::getObjectManager(); +$quote = $objectManager->create(Quote::class); +$quote->load('test_guest_order_with_gift_message', 'reserved_order_id'); +$message = $objectManager->create(Message::class); +$product = $objectManager->create(Product::class); +foreach ($quote->getAllItems() as $item) { + $message->load($item->getGiftMessageId()); + $message->delete(); + $sku = $item->getSku(); + $product->load($product->getIdBySku($sku)); + if ($product->getId()) { + $product->delete(); + } +} +$quote->delete(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php new file mode 100644 index 0000000000000..394b13078010a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +return [ + 'region' => 'CA', + 'region_id' => '12', + 'postcode' => '11111', + 'lastname' => 'lastname', + 'firstname' => 'firstname', + 'street' => 'street', + 'city' => 'Los Angeles', + 'email' => 'admin@example.com', + 'telephone' => '11111111', + 'country_id' => 'US' +]; diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php new file mode 100644 index 0000000000000..8e9161f1ec628 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$addressData = include __DIR__ . '/address_data.php'; + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple'); + +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setBaseRowTotal($product->getPrice()); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setOrderCurrencyCode("USD") + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_rollback.php new file mode 100644 index 0000000000000..113f84dae385e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php new file mode 100644 index 0000000000000..f1124ba135285 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Model\Order; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order.php'); +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +/** @var Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); +$payment = $order->getPayment(); +$orderItems = $order->getItems(); +$orderItem = reset($orderItems); +$addressData = include __DIR__ . '/address_data.php'; +$orders = [ + [ + 'increment_id' => '100000002', + 'state' => \Magento\Sales\Model\Order::STATE_NEW, + 'status' => 'processing', + 'order_currency_code' =>'USD', + 'grand_total' => 120.00, + 'subtotal' => 120.00, + 'base_grand_total' => 120.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000003', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'processing', + 'grand_total' => 130.00, + 'base_grand_total' => 130.00, + 'subtotal' => 130.00, + 'total_paid' => 130.00, + 'store_id' => 0, + 'website_id' => 0, + ], + [ + 'increment_id' => '100000004', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'closed', + 'grand_total' => 140.00, + 'base_grand_total' => 140.00, + 'subtotal' => 140.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000005', + 'state' => \Magento\Sales\Model\Order::STATE_COMPLETE, + 'status' => 'complete', + 'grand_total' => 150.00, + 'base_grand_total' => 150.00, + 'subtotal' => 150.00, + 'total_paid' => 150.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000006', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'grand_total' => 160.00, + 'base_grand_total' => 160.00, + 'subtotal' => 160.00, + 'total_paid' => 160.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000007', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'order_currency_code' =>'USD', + 'grand_total' => 180.00, + 'base_grand_total' => 180.00, + 'subtotal' => 170.00, + 'tax_amount' => 5.00, + 'shipping_amount'=> 5.00, + 'base_shipping_amount'=> 4.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000008', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'order_currency_code' =>'USD', + 'grand_total' => 190.00, + 'base_grand_total' => 190.00, + 'subtotal' => 180.00, + 'tax_amount' => 5.00, + 'shipping_amount'=> 5.00, + 'base_shipping_amount'=> 4.00, + 'store_id' => 1, + 'website_id' => 1, + ] +]; + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +/** @var array $orderData */ +foreach ($orders as $orderData) { + $newPayment = clone $payment; + $newPayment->setId(null); + /** @var $order \Magento\Sales\Model\Order */ + $order = $objectManager->create( + \Magento\Sales\Model\Order::class + ); + + // Reset addresses + /** @var Order\Address $billingAddress */ + $billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType('billing'); + + $shippingAddress = clone $billingAddress; + $shippingAddress->setId(null)->setAddressType('shipping'); + + /** @var Order\Item $orderItem */ + $orderItem = $objectManager->create(Order\Item::class); + $orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + + + $order->setData($orderData) + ->addItem($orderItem) + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setPayment($newPayment); + + $orderRepository->save($order); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php new file mode 100644 index 0000000000000..dc455c3cb2c49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php new file mode 100644 index 0000000000000..cb61f5e398630 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store.php'); + +/** @var \Magento\Catalog\Model\Product $product */ + +$addressData = include __DIR__ . '/address_data.php'; + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple'); + +$secondStore = Bootstrap::getObjectManager() + ->create(Store::class); + +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); +$customerIdFromFixture = 1; +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setOrderCurrencyCode("USD") + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(10) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(false) + ->setCustomerId($customerIdFromFixture) + ->setCustomerEmail('customer@null.com') + ->setOrderCurrencyCode('USD') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); + +/** @var Payment $payment */ +$secondPayment = $objectManager->create(Payment::class); +$secondPayment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$secondOrderItem = $objectManager->create(OrderItem::class); +$secondOrderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + +/** @var Order $order */ +$secondOrder = $objectManager->create(Order::class); +$secondOrder->setIncrementId('100000002') + ->setState(Order::STATE_PROCESSING) + ->setStatus($secondOrder->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setOrderCurrencyCode("USD") + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(110) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(false) + ->setCustomerId($customerIdFromFixture) + ->setCustomerEmail('customer@null.com') + ->setOrderCurrencyCode('USD') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($secondStore->load('fixture_second_store')->getId()) + ->addItem($secondOrderItem) + ->setPayment($secondPayment); +$orderRepository->save($secondOrder); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php new file mode 100644 index 0000000000000..fe98d8659d3c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php new file mode 100644 index 0000000000000..fbd710fc07c0c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php @@ -0,0 +1,24 @@ +<?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); + +//configuration setting for shipping tax class and shipping tax calculation and display +$configWriter->save('tax/classes/shipping_tax_class', '2'); +$configWriter->save('tax/calculation/shipping_includes_tax', '1'); +$configWriter->save('tax/sales_display/shipping', '3'); +$configWriter->save('tax/display/shipping', '3'); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_rollback.php new file mode 100644 index 0000000000000..21b0a4317fc78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_rollback.php @@ -0,0 +1,23 @@ +<?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); + +//Apply discount on prices to include tax +$configWriter->save('tax/classes/shipping_tax_class', '0'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '1'); +$configWriter->save('tax/display/shipping', '1'); +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php new file mode 100644 index 0000000000000..9e1ce11a01b0e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php @@ -0,0 +1,24 @@ +<?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); + +//configuration setting for shipping tax class and shipping tax calculation and display +$configWriter->save('tax/classes/shipping_tax_class', '2'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '3'); +$configWriter->save('tax/display/shipping', '3'); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_rollback.php new file mode 100644 index 0000000000000..21b0a4317fc78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_rollback.php @@ -0,0 +1,23 @@ +<?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); + +//Apply discount on prices to include tax +$configWriter->save('tax/classes/shipping_tax_class', '0'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '1'); +$configWriter->save('tax/display/shipping', '1'); +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php new file mode 100644 index 0000000000000..2603e2056f19d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\Data\TaxRateInterface; +use Magento\Tax\Api\Data\TaxRuleInterface; +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\DataObjectHelper; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var Rate $rate */ +$rate = $rateFactory->create(); +$rateData = [ + Rate::KEY_COUNTRY_ID => 'US', + Rate::KEY_REGION_ID => '1', + Rate::KEY_POSTCODE => '*', + Rate::KEY_CODE => 'US-AL-*-Rate-1', + Rate::KEY_PERCENTAGE_RATE => '5.5', +]; +$dataObjectHelper->populateWithArray($rate, $rateData, TaxRateInterface::class); +$rateRepository->save($rate); + +$rule = $ruleFactory->create(); +$ruleData = [ + Rule::KEY_CODE=> 'GraphQl Test Rule AL', + Rule::KEY_PRIORITY => '0', + Rule::KEY_POSITION => '0', + Rule::KEY_CUSTOMER_TAX_CLASS_IDS => [3], + Rule::KEY_PRODUCT_TAX_CLASS_IDS => [2], + Rule::KEY_TAX_RATE_IDS => [$rate->getId()], +]; +$dataObjectHelper->populateWithArray($rule, $ruleData, TaxRuleInterface::class); +$ruleRepository->save($rule); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php new file mode 100644 index 0000000000000..22372f3a21022 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tax\Model\ResourceModel\Calculation\Rate as RateResource; +use Magento\Tax\Model\ResourceModel\Calculation\Rule as RuleResource; + +$objectManager = Bootstrap::getObjectManager(); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var RateResource $rateResource */ +$rateResource = $objectManager->get(RateResource::class); +/** @var RuleResource $ruleResource */ +$ruleResource = $objectManager->get(RuleResource::class); + +$rate = $rateFactory->create(); +$rateResource->load($rate, 'US-AL-*-Rate-1', Rate::KEY_CODE); +$rule = $ruleFactory->create(); +$ruleResource->load($rule, 'GraphQl Test Rule AL', Rule::KEY_CODE); +$ruleRepository->delete($rule); +$rateRepository->delete($rate); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php index 9d83b3d2ece98..1bd41b047163a 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php @@ -9,6 +9,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\ImportExport\Model\Import; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -28,43 +29,53 @@ class CsvTest extends TestCase */ private $objectManager; - /** - * @var Csv - */ - private $csv; - /** * @inheritdoc */ protected function setUp(): void { - parent::setUp(); - $this->objectManager = Bootstrap::getObjectManager(); - $this->csv = $this->objectManager->create( - Csv::class, - ['destination' => $this->destination] - ); } /** * Test to destruct export adapter + * + * @dataProvider destructDataProvider + * + * @param string $destination + * @param bool $shouldBeDeleted + * @return void */ - public function testDestruct(): void + public function testDestruct(string $destination, bool $shouldBeDeleted): void { + $csv = $this->objectManager->create(Csv::class, ['destination' => $destination]); /** @var Filesystem $fileSystem */ $fileSystem = $this->objectManager->get(Filesystem::class); $directoryHandle = $fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); /** Assert that the destination file is present after construct */ $this->assertFileExists( - $directoryHandle->getAbsolutePath($this->destination), + $directoryHandle->getAbsolutePath($destination), 'The destination file was\'t created after construct' ); - /** Assert that the destination file was removed after destruct */ - $this->csv = null; - $this->assertFileNotExists( - $directoryHandle->getAbsolutePath($this->destination), - 'The destination file was\'t removed after destruct' - ); + unset($csv); + + if ($shouldBeDeleted) { + $this->assertFileDoesNotExist($directoryHandle->getAbsolutePath($destination)); + } else { + $this->assertFileExists($directoryHandle->getAbsolutePath($destination)); + } + } + + /** + * DataProvider for testDestruct + * + * @return array + */ + public function destructDataProvider(): array + { + return [ + 'temporary file' => [$this->destination, true], + 'import history file' => [Import::IMPORT_HISTORY_DIR . $this->destination, false], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php index dd4fdde250c03..b6508e3b3dfda 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php @@ -53,7 +53,7 @@ public function testGetFilters(): void ['is_filterable' => '1'], [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 1], ], 'Category 1' ); diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php index 07882b68d62d5..e226881b9cfcc 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php @@ -76,7 +76,7 @@ public function getFiltersDataProvider(): array ], [ 'label' => '<span class="price">$60.00</span> and above', - 'value' => '60-', + 'value' => '60-70', 'count' => 1, ], ], @@ -94,7 +94,7 @@ public function getFiltersDataProvider(): array ], [ 'label' => '<span class="price">$50.00</span> and above', - 'value' => '50-', + 'value' => '50-60', 'count' => 1, ], ], diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php index 3b2673b18635a..97928463620f4 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php @@ -71,15 +71,15 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], ['label' => '$20.00 - $29.99', 'value' => '20-30', 'count' => 1], - ['label' => '$50.00 and above', 'value' => '50-', 'count' => 1], + ['label' => '$50.00 and above', 'value' => '50-60', 'count' => 1], ], ], 'auto_calculation_variation_with_big_price_difference' => [ 'config' => ['catalog/layered_navigation/price_range_calculation' => 'auto'], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 300.00], 'expectation' => [ - ['label' => '$0.00 - $99.99', 'value' => '-100', 'count' => 2], - ['label' => '$300.00 and above', 'value' => '300-', 'count' => 1], + ['label' => '$0.00 - $99.99', 'value' => '0-100', 'count' => 2], + ['label' => '$300.00 and above', 'value' => '300-400', 'count' => 1], ], ], 'auto_calculation_variation_with_fixed_price_step' => [ @@ -88,7 +88,7 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$300.00 - $399.99', 'value' => '300-400', 'count' => 1], ['label' => '$400.00 - $499.99', 'value' => '400-500', 'count' => 1], - ['label' => '$500.00 and above', 'value' => '500-', 'count' => 1], + ['label' => '$500.00 and above', 'value' => '500-600', 'count' => 1], ], ], 'improved_calculation_variation_with_small_price_difference' => [ @@ -98,8 +98,8 @@ public function getFiltersDataProvider(): array ], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 50.00], 'expectation' => [ - ['label' => '$0.00 - $49.99', 'value' => '-50', 'count' => 2], - ['label' => '$50.00 and above', 'value' => '50-', 'count' => 1], + ['label' => '$0.00 - $19.99', 'value' => '0-20', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-50', 'count' => 2], ], ], 'improved_calculation_variation_with_big_price_difference' => [ @@ -109,8 +109,8 @@ public function getFiltersDataProvider(): array ], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 300.00], 'expectation' => [ - ['label' => '$0.00 - $299.99', 'value' => '-300', 'count' => 2.0], - ['label' => '$300.00 and above', 'value' => '300-', 'count' => 1.0], + ['label' => '$0.00 - $19.99', 'value' => '0-20', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-300', 'count' => 2], ], ], 'manual_calculation_with_price_step_200' => [ @@ -121,7 +121,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 300.00, 'simple1001' => 300.00, 'simple1002' => 500.00], 'expectation' => [ ['label' => '$200.00 - $399.99', 'value' => '200-400', 'count' => 2], - ['label' => '$400.00 and above', 'value' => '400-', 'count' => 1], + ['label' => '$400.00 and above', 'value' => '400-600', 'count' => 1], ], ], 'manual_calculation_with_price_step_10' => [ @@ -132,7 +132,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 300.00, 'simple1001' => 300.00, 'simple1002' => 500.00], 'expectation' => [ ['label' => '$300.00 - $309.99', 'value' => '300-310', 'count' => 2], - ['label' => '$500.00 and above', 'value' => '500-', 'count' => 1], + ['label' => '$500.00 and above', 'value' => '500-510', 'count' => 1], ], ], 'manual_calculation_with_number_of_intervals_10' => [ @@ -145,7 +145,7 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], ['label' => '$20.00 - $29.99', 'value' => '20-30', 'count' => 1], - ['label' => '$30.00 and above', 'value' => '30-', 'count' => 1], + ['label' => '$30.00 and above', 'value' => '30-40', 'count' => 1], ], ], 'manual_calculation_with_number_of_intervals_2' => [ @@ -157,7 +157,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 30.00], 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 2], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 2], ], ], ]; diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php index 435dd29e16dfa..760f4031b8844 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php @@ -32,7 +32,7 @@ public function testGetFilters(): void ['is_filterable_in_search' => 1], [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 1], ] ); } diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php similarity index 62% rename from dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php rename to dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php index f63674754ea3d..bd0df51162620 100644 --- a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php @@ -7,18 +7,17 @@ namespace Magento\MediaGallery\Model; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** - * Test for IsPathBlacklistedInterface + * Test for IsPathExcludedInterface */ -class IsBlacklistedTest extends TestCase +class IsExcludedTest extends TestCase { - /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ private $service; @@ -27,23 +26,23 @@ class IsBlacklistedTest extends TestCase */ protected function setUp(): void { - $this->service = Bootstrap::getObjectManager()->get(IsPathBlacklistedInterface::class); + $this->service = Bootstrap::getObjectManager()->get(IsPathExcludedInterface::class); } /** - * Testing the blacklisted paths + * Testing the excluded paths * * @param string $path - * @param bool $isBlacklisted + * @param bool $isExcluded * @dataProvider pathsProvider */ - public function testExecute(string $path, bool $isBlacklisted): void + public function testExecute(string $path, bool $isExcluded): void { - $this->assertEquals($isBlacklisted, $this->service->execute($path)); + $this->assertEquals($isExcluded, $this->service->execute($path)); } /** - * Provider of paths and if the path should be in the blacklist + * Provider of paths and if the path should be in the excluded list * * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php new file mode 100644 index 0000000000000..3aadad7e9ebec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Product\Plugin; + +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Tests for update quote items plugin + * + * @magentoAppArea adminhtml + */ +class UpdateQuoteItemsTest extends TestCase +{ + /** + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); + $this->productRepository = $objectManager->get(ProductRepository::class); + } + + /** + * Test to mark the quote as need to recollect and doesn't update the field "updated_at" after change product price + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @return void + */ + public function testMarkQuoteRecollectAfterChangeProductPrice(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->assertNotNull($quote); + $this->assertFalse((bool)$quote->getTriggerRecollect()); + $this->assertNotEmpty($quote->getItems()); + $quoteItem = current($quote->getItems()); + $product = $quoteItem->getProduct(); + + $product->setPrice((float)$product->getPrice() + 10); + $this->productRepository->save($product); + + /** @var AdapterInterface $connection */ + $connection = $quote->getResource()->getConnection(); + $select = $connection->select() + ->from( + $connection->getTableName('quote'), + ['updated_at', 'trigger_recollect'] + )->where( + "reserved_order_id = 'test_order_with_simple_product_without_address'" + ); + + $quoteRow = $connection->fetchRow($select); + $this->assertNotEmpty($quoteRow); + $this->assertTrue((bool)$quoteRow['trigger_recollect']); + $this->assertEquals($quote->getUpdatedAt(), $quoteRow['updated_at']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php index 72e741493d8f8..bc51f8acb2f6f 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -5,10 +5,33 @@ */ namespace Magento\Sales\Model\Order\Email\Sender; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity; use Magento\TestFramework\Helper\Bootstrap; class CreditmemoSenderTest extends \PHPUnit\Framework\TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const OLD_CUSTOMER_EMAIL = 'customer@null.com'; + const ORDER_EMAIL = 'customer@null.com'; + + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->customerRepository = Bootstrap::getObjectManager() + ->get(CustomerRepositoryInterface::class); + } + /** * @magentoDataFixture Magento/Sales/_files/order.php */ @@ -35,4 +58,110 @@ public function testSend() $this->assertTrue($result); $this->assertNotEmpty($creditmemo->getEmailSent()); } + + /** + * Test that when a customer email is modified, the credit memo is sent to the new email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasModified() + { + $customer = $this->customerRepository->getById(1); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + $this->customerRepository->save($customer); + + $order = $this->createOrder(); + $creditmemo = $this->createCreditmemo($order); + + $this->assertEmpty($creditmemo->getEmailSent()); + + $craditmemoIdentity = $this->createCreditMemoIdentity(); + $creditmemoSender = $this->createCreditMemoSender($craditmemoIdentity); + $result = $creditmemoSender->send($creditmemo, true); + + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $craditmemoIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($creditmemo->getEmailSent()); + } + + /** + * Test that when a customer email is not modified, the credit memo is sent to the old customer email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasNotModified() + { + $order = $this->createOrder(); + $creditmemo = $this->createCreditmemo($order); + + $this->assertEmpty($creditmemo->getEmailSent()); + + $craditmemoIdentity = $this->createCreditMemoIdentity(); + $creditmemoSender = $this->createCreditMemoSender($craditmemoIdentity); + $result = $creditmemoSender->send($creditmemo, true); + + $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $craditmemoIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($creditmemo->getEmailSent()); + } + + /** + * Test that when an order has not customer the credit memo is sent to the order email + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoAppArea frontend + */ + public function testSendWithoutCustomer() + { + $order = $this->createOrder(); + $creditmemo = $this->createCreditmemo($order); + + $this->assertEmpty($creditmemo->getEmailSent()); + + $creditmemoIdentity = $this->createCreditMemoIdentity(); + $creditmemoSender = $this->createCreditMemoSender($creditmemoIdentity); + $result = $creditmemoSender->send($creditmemo, true); + + $this->assertEquals(self::ORDER_EMAIL, $creditmemoIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($creditmemo->getEmailSent()); + } + + private function createCreditmemo(Order $order): Order\Creditmemo + { + $creditmemo = Bootstrap::getObjectManager()->create( + \Magento\Sales\Model\Order\Creditmemo::class + ); + $creditmemo->setOrder($order); + return $creditmemo; + } + + private function createOrder(): Order + { + $order = Bootstrap::getObjectManager() + ->create(Order::class); + $order->loadByIncrementId('100000001'); + + return $order; + } + + private function createCreditMemoIdentity(): CreditmemoIdentity + { + return Bootstrap::getObjectManager()->create( + CreditmemoIdentity::class + ); + } + + private function createCreditMemoSender(CreditmemoIdentity $creditmemoIdentity): CreditmemoSender + { + return Bootstrap::getObjectManager() + ->create( + CreditmemoSender::class, + [ + 'identityContainer' => $creditmemoIdentity, + ] + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php index fa3421fe9cc94..60021c7086267 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -5,8 +5,35 @@ */ namespace Magento\Sales\Model\Order\Email\Sender; -class InvoiceSenderTest extends \PHPUnit\Framework\TestCase +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; +use Magento\Sales\Model\Order\Invoice; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class InvoiceSenderTest extends TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const OLD_CUSTOMER_EMAIL = 'customer@null.com'; + const ORDER_EMAIL = 'customer@null.com'; + + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->customerRepository = Bootstrap::getObjectManager() + ->get(CustomerRepositoryInterface::class); + } + /** * @magentoDataFixture Magento/Sales/_files/order.php */ @@ -34,4 +61,111 @@ public function testSend() $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); } + + /** + * Test that when a customer email is modified, the invoice is sent to the new email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasModified() + { + $customer = $this->customerRepository->getById(1); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + $this->customerRepository->save($customer); + + $order = $this->createOrder(); + $invoice = $this->createInvoice($order); + $invoiceIdentity = $this->createInvoiceEntity(); + $invoiceSender = $this->createInvoiceSender($invoiceIdentity); + + $this->assertEmpty($invoice->getEmailSent()); + $result = $invoiceSender->send($invoice, true); + + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($invoice->getEmailSent()); + } + + /** + * Test that when a customer email is not modified, the invoice is sent to the old customer email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasNotModified() + { + $order = $this->createOrder(); + $invoice = $this->createInvoice($order); + $invoiceIdentity = $this->createInvoiceEntity(); + $invoiceSender = $this->createInvoiceSender($invoiceIdentity); + + $this->assertEmpty($invoice->getEmailSent()); + $result = $invoiceSender->send($invoice, true); + + $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($invoice->getEmailSent()); + } + + /** + * Test that when an order has not customer the invoice is sent to the order email + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoAppArea frontend + */ + public function testSendWithoutCustomer() + { + $order = $this->createOrder(); + $invoice = $this->createInvoice($order); + + /** @var InvoiceIdentity $invoiceIdentity */ + $invoiceIdentity = $this->createInvoiceEntity(); + /** @var InvoiceSender $invoiceSender */ + $invoiceSender = $this->createInvoiceSender($invoiceIdentity); + + $this->assertEmpty($invoice->getEmailSent()); + $result = $invoiceSender->send($invoice, true); + + $this->assertEquals(self::ORDER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($invoice->getEmailSent()); + } + + private function createInvoice(Order $order): Invoice + { + $invoice = Bootstrap::getObjectManager()->create( + Invoice::class + ); + $invoice->setOrder($order); + + return $invoice; + } + + private function createOrder(): Order + { + $order = Bootstrap::getObjectManager() + ->create(Order::class); + $order->loadByIncrementId('100000001'); + + return $order; + } + + private function createInvoiceEntity(): InvoiceIdentity + { + return Bootstrap::getObjectManager()->create( + InvoiceIdentity::class + ); + } + + private function createInvoiceSender(InvoiceIdentity $invoiceIdentity): InvoiceSender + { + return Bootstrap::getObjectManager() + ->create( + InvoiceSender::class, + [ + 'identityContainer' => $invoiceIdentity, + ] + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/ShipmentSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/ShipmentSenderTest.php index 83bc7e10647b4..42d8e2bc0bcbb 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/ShipmentSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/ShipmentSenderTest.php @@ -5,6 +5,11 @@ */ namespace Magento\Sales\Model\Order\Email\Sender; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Container\ShipmentIdentity; +use Magento\Sales\Model\Order\Shipment; use Magento\Sales\Model\Order\ShipmentFactory; use Magento\TestFramework\Helper\Bootstrap; @@ -16,6 +21,25 @@ */ class ShipmentSenderTest extends \PHPUnit\Framework\TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const OLD_CUSTOMER_EMAIL = 'customer@null.com'; + const ORDER_EMAIL = 'customer@null.com'; + + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->customerRepository = Bootstrap::getObjectManager() + ->get(CustomerRepositoryInterface::class); + } + /** * @magentoDataFixture Magento/Sales/_files/order.php */ @@ -39,6 +63,76 @@ public function testSend() $this->assertNotEmpty($shipment->getEmailSent()); } + /** + * Test that when a customer email is modified, the shipment is sent to the new email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasModified() + { + $customer = $this->customerRepository->getById(1); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + $this->customerRepository->save($customer); + + $order = $this->createOrder(); + $shipment = $this->createShipment($order); + $shipmentIdentity = $this->createShipmentEntity(); + $shipmentSender = $this->createShipmentSender($shipmentIdentity); + + $this->assertEmpty($shipment->getEmailSent()); + $result = $shipmentSender->send($shipment, true); + + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $shipmentIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($shipment->getEmailSent()); + } + + /** + * Test that when a customer email is not modified, the shipment is sent to the old customer email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasNotModified() + { + $order = $this->createOrder(); + $shipment = $this->createShipment($order); + $shipmentIdentity = $this->createShipmentEntity(); + $shipmentSender = $this->createShipmentSender($shipmentIdentity); + + $this->assertEmpty($shipment->getEmailSent()); + $result = $shipmentSender->send($shipment, true); + + $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $shipmentIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($shipment->getEmailSent()); + } + + /** + * Test that when an order has not customer the shipment is sent to the order email + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoAppArea frontend + */ + public function testSendWithoutCustomer() + { + $order = $this->createOrder(); + $shipment = $this->createShipment($order); + + /** @var ShipmentIdentity $shipmentIdentity */ + $shipmentIdentity = $this->createShipmentEntity(); + /** @var ShipmentSender $shipmentSender */ + $shipmentSender = $this->createShipmentSender($shipmentIdentity); + + $this->assertEmpty($shipment->getEmailSent()); + $result = $shipmentSender->send($shipment, true); + + $this->assertEquals(self::ORDER_EMAIL, $shipmentIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($shipment->getEmailSent()); + } + /** * Check the correctness and stability of set/get packages of shipment * @@ -65,4 +159,41 @@ public function testPackages() $shipment->load($shipment->getId()); $this->assertEquals($packages, $shipment->getPackages()); } + + private function createShipment(Order $order): Shipment + { + $shipment = Bootstrap::getObjectManager()->create( + Shipment::class + ); + $shipment->setOrder($order); + + return $shipment; + } + + private function createOrder(): Order + { + $order = Bootstrap::getObjectManager() + ->create(Order::class); + $order->loadByIncrementId('100000001'); + + return $order; + } + + private function createShipmentEntity(): ShipmentIdentity + { + return Bootstrap::getObjectManager()->create( + ShipmentIdentity::class + ); + } + + private function createShipmentSender(ShipmentIdentity $shipmentIdentity): ShipmentSender + { + return Bootstrap::getObjectManager() + ->create( + ShipmentSender::class, + [ + 'identityContainer' => $shipmentIdentity, + ] + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php new file mode 100644 index 0000000000000..48fbdefb2cdf8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'year' => '2015', + 'month' => '9', + 'day' => '9', + 'hour' => '2', + 'minute' => '2', + 'day_part' => 'am', + 'date_internal' => '', + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product2 = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product2 = $repository->get('simple_with_cross'); + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setName($product->getName()); +$orderItem->setSku($product->getSku()); +$orderItem->setQtyOrdered(1); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order\Item $orderItem2 */ +$orderItem2 = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem2->setProductId($product2->getId()); +$orderItem2->setSku($product2->getSku()); +$orderItem2->setName($product2->getName()); +$orderItem2->setQtyOrdered(1); +$orderItem2->setBasePrice($product2->getPrice()); +$orderItem2->setPrice($product2->getPrice()); +$orderItem2->setRowTotal($product2->getPrice()); +$orderItem2->setProductType($product2->getTypeId()); +$orderItem2->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000001'); +$order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_PROCESSING)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@null.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->addItem($orderItem2); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(100); +$order->setBaseSubtotal(100); +$order->setBaseGrandTotal(100); +$order->setGrandTotal(100); +$order->setOrderCurrencyCode('USD'); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); + +$orderService = $objectManager->create( + \Magento\Sales\Api\InvoiceManagementInterface::class +); +$invoice = $orderService->prepareInvoice($order); +$invoice->register(); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options_rollback.php new file mode 100644 index 0000000000000..80d6adb0cd9fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php new file mode 100644 index 0000000000000..996a2598dc02b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'year' => '2015', + 'month' => '9', + 'day' => '9', + 'hour' => '2', + 'minute' => '2', + 'day_part' => 'am', + 'date_internal' => '', + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product2 = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product2 = $repository->get('simple_with_cross'); + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setName($product->getName()); +$orderItem->setSku($product->getSku()); +$orderItem->setQtyOrdered(4); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order\Item $orderItem2 */ +$orderItem2 = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem2->setProductId($product2->getId()); +$orderItem2->setSku($product2->getSku()); +$orderItem2->setName($product2->getName()); +$orderItem2->setQtyOrdered(1); +$orderItem2->setBasePrice($product2->getPrice()); +$orderItem2->setPrice($product2->getPrice()); +$orderItem2->setRowTotal($product2->getPrice()); +$orderItem2->setProductType($product2->getTypeId()); +$orderItem2->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000002'); +$order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_PROCESSING)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@null.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->addItem($orderItem2); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(50); +$order->setBaseSubtotal(50); +$order->setBaseGrandTotal(50); +$order->setGrandTotal(50); +$order->setOrderCurrencyCode('USD'); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); + +$orderService = $objectManager->create( + \Magento\Sales\Api\InvoiceManagementInterface::class +); +/** @var \Magento\Sales\Api\Data\InvoiceInterface $invoice */ +$invoice = $orderService->prepareInvoice($order, [$orderItem->getId() => 3]); +$invoice->register(); +$invoice->setGrandTotal(50); +$invoice->setSubTotal(30); +$invoice->setShippingInclTax(20); +$invoice->setShippingAmount(20); +$invoice->setBaseShippingAmount(20); +$invoice->setShippingInclTax(25); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); + +$invoice = $orderService->prepareInvoice($order, [$orderItem2->getId() => 1]); +$invoice->register(); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options_rollback.php new file mode 100644 index 0000000000000..80d6adb0cd9fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices.php new file mode 100644 index 0000000000000..3894082c20030 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/three_customers.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +$payment2 = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment2->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'year' => '2015', + 'month' => '9', + 'day' => '9', + 'hour' => '2', + 'minute' => '2', + 'day_part' => 'am', + 'date_internal' => '', + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setSku($product->getSku()); +$orderItem->setName($product->getName()); +$orderItem->setQtyOrdered(1); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000001'); +$order->setState(\Magento\Sales\Model\Order::STATE_NEW); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_NEW)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@null.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(100); +$order->setBaseSubtotal(100); +$order->setBaseGrandTotal(100); +$order->setOrderCurrencyCode('USD'); +$order->setGrandTotal(100); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); + +$orderService = $objectManager->create( + \Magento\Sales\Api\InvoiceManagementInterface::class +); +$invoice = $orderService->prepareInvoice($order); +$invoice->register(); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem2 = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem2->setProductId($product->getId()); +$orderItem2->setSku($product->getSku()); +$orderItem2->setQtyOrdered(1); +$orderItem2->setBasePrice($product->getPrice()); +$orderItem2->setPrice($product->getPrice()); +$orderItem2->setRowTotal($product->getPrice()); +$orderItem2->setProductType($product->getTypeId()); +$orderItem2->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order2 = $objectManager->create(\Magento\Sales\Model\Order::class); +$order2->setIncrementId('100000002'); +$order2->setState(\Magento\Sales\Model\Order::STATE_NEW); +$order2->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_NEW)); +$order2->setCustomerIsGuest(true); +$order2->setCustomerEmail('customer@null.com'); +$order2->setCustomerFirstname('firstname'); +$order2->setCustomerLastname('lastname'); +$order2->setBillingAddress($billingAddress); +$order2->setShippingAddress($shippingAddress); +$order2->setAddresses([$billingAddress, $shippingAddress]); +$order2->setPayment($payment2); +$order2->addItem($orderItem2); +$order2->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order2->setSubtotal(100); +$order2->setBaseSubtotal(100); +$order2->setBaseGrandTotal(100); +$order2->setCustomerId(2) + ->setCustomerIsGuest(false) + ->save(); + +$invoice2 = $orderService->prepareInvoice($order2); +$invoice2->register(); +$order2 = $invoice2->getOrder(); +$order2->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order2)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices_rollback.php new file mode 100644 index 0000000000000..29c6c3b26a7c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/three_customers_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_for_two_diff_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_for_two_diff_customers_rollback.php new file mode 100644 index 0000000000000..570c6f777f198 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_for_two_diff_customers_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/two_customers_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php new file mode 100644 index 0000000000000..6ac4f65f36e5f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Model\Rule; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$websiteId = $objectManager->get(StoreManagerInterface::class) + ->getWebsite() + ->getId(); + +/** @var Rule $salesRule */ +$salesRule = $objectManager->create(Rule::class); +$salesRule->setData( + [ + 'name' => '10% Off on orders with shipping discount', + 'is_active' => 1, + 'customer_group_ids' => [1], + 'coupon_type' => Rule::COUPON_TYPE_NO_COUPON, + 'simple_action' => 'by_percent', + 'discount_amount' => 10, + 'discount_step' => 0, + 'apply_to_shipping' => 1, + 'stop_rules_processing' => 1, + 'website_ids' => [$websiteId], + 'store_labels' => [ + 'store_id' => 0, + 'store_label' => 'Discount Label for 10% off', + ] + ] +); + +$salesRule->getConditions()->loadArray([ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'base_subtotal', + 'operator' => '>=', + 'value' => '20', + 'is_value_processed' => false, + 'actions' => [ + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator'=>'all' + ], + ], + ], + ], +]); + +$salesRule->save(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping_rollback.php new file mode 100644 index 0000000000000..f5de93e529b22 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/SalesRule/_files/rules_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php index e630ab0f83ce2..e39d4d7331a40 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php @@ -16,7 +16,8 @@ /** * Class GenerateFixturesCommandCommandTest - * @package Magento\Setup\Console\Command + * + * @magentoDbIsolation disabled */ class GenerateFixturesCommandTest extends \Magento\TestFramework\Indexer\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/PriceIndexerDimensionsModeSetCommandTest.php b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/PriceIndexerDimensionsModeSetCommandTest.php index e95837a65c77b..1d589d73b3762 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/PriceIndexerDimensionsModeSetCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/PriceIndexerDimensionsModeSetCommandTest.php @@ -13,6 +13,8 @@ /** * Test command that sets indexer mode for catalog_product_price indexer + * + * @magentoDbIsolation disabled */ class PriceIndexerDimensionsModeSetCommandTest extends \Magento\TestFramework\Indexer\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php b/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php index 3322e30780b4d..9737467422aba 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php @@ -19,7 +19,9 @@ use Magento\TestFramework\ObjectManager; /** - * Class WhitelistDeclarationTest + * Checks whitelisted tables behaviour + * + * @magentoDbIsolation disabled */ class WhitelistDeclarationTest extends \PHPUnit\Framework\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php index 3069f682f9688..2829cffd8d8a7 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php @@ -13,6 +13,8 @@ /** * Class Application test * + * @magentoDbIsolation disabled + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FixtureModelTest extends \Magento\TestFramework\Indexer\TestCase diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php new file mode 100644 index 0000000000000..326ec789da45a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class FixturesAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php new file mode 100644 index 0000000000000..e0049895577cc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +/** + * Test interface for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface FixturesInterface +{ + +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php new file mode 100644 index 0000000000000..8679c254aa73f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that fixtures override config inherited from abstract class and interface. + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class FixturesTest extends FixturesAbstractClass implements FixturesInterface +{ + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var FixtureCallStorage + */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * @magentoAdminConfigFixture test_section/test_group/field_2 new_value + * @magentoAdminConfigFixture test_section/test_group/field_3 new_value + * @magentoConfigFixture current_store test_section/test_group/field_2 new_value + * @magentoConfigFixture current_store test_section/test_group/field_3 new_value + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * @magentoDataFixtureBeforeTransaction Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixtureBeforeTransaction Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * @dataProvider interfaceDataProvider + * @param array $configs + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testInterfaceInheritance( + array $configs, + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($configs); + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @magentoAdminConfigFixture test_section/test_group/field_2 new_value + * @magentoConfigFixture current_store test_section/test_group/field_2 new_value + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixtureBeforeTransaction Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @dataProvider abstractDataProvider + * @param array $configs + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testAbstractInheritance( + array $configs, + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($configs); + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @return array + */ + public function interfaceDataProvider(): array + { + return [ + 'first_data_set' => [ + 'admin_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => 'overridden config fixture value for method', + 'test_section/test_group/field_3' => 'new_value', + ], + 'store_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => 'overridden config fixture value for method', + 'test_section/test_group/field_3' => 'new_value', + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 2, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 2, + 'fixture3_first_module.php' => 2, + ], + ], + 'second_data_set' => [ + 'admin_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => 'overridden config fixture value for method', + 'test_section/test_group/field_3' => '3rd field default value', + ], + 'store_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => 'overridden config fixture value for method', + 'test_section/test_group/field_3' => '3rd field website scope default value', + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 2, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 2, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * @return array + */ + public function abstractDataProvider(): array + { + return [ + 'first_data_set' => [ + 'admin_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => '2nd field default value', + 'test_section/test_group/field_3' => 'overridden config fixture value for data set from abstract', + ], + 'store_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => '2nd field default value', + 'test_section/test_group/field_3' => 'overridden config fixture value for data set from abstract', + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 2, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 0, + 'fixture3_first_module.php' => 2, + ], + ], + 'second_data_set' => [ + 'admin_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for data set from abstract', + 'test_section/test_group/field_2' => '2nd field default value', + 'test_section/test_group/field_3' => '3rd field default value', + ], + 'store_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for data set from abstract', + 'test_section/test_group/field_2' => '2nd field default value', + 'test_section/test_group/field_3' => '3rd field website scope default value', + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 0, + 'fixture2_first_module.php' => 0, + 'fixture1_second_module.php' => 2, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * Asserts config field values. + * + * @param array $configs + * @param string $scope + * @return void + */ + private function assertConfigFieldValues( + array $configs, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ): void { + foreach ($configs as $path => $expectedValue) { + $this->assertEquals($expectedValue, $this->config->getValue($path, $scope)); + } + } + + /** + * Asserts count of used fixtures. + * + * @param array $fixtures + * @return void + */ + private function assertUsedFixturesCount(array $fixtures): void + { + foreach ($fixtures as $fixture => $count) { + $this->assertEquals($count, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php new file mode 100644 index 0000000000000..445aa0c501c0a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class SkipAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php new file mode 100644 index 0000000000000..99a9332460211 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Test interface for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface SkipInterface +{ + +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php new file mode 100644 index 0000000000000..e5eb1e3a419f7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Class checks that test method can be skipped using inherited from abstract class/interface override config + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class SkipTest extends SkipAbstractClass implements SkipInterface +{ + /** + * @return void + */ + public function testAbstractSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from abstract class'); + } + + /** + * @return void + */ + public function testInterfaceSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from interface'); + } + + /** + * @dataProvider skipDataProvider + * + * @param string $message + * @return void + */ + public function testSkipDataSet(string $message): void + { + $this->fail($message); + } + + /** + * @return array + */ + public function skipDataProvider(): array + { + return [ + 'first_data_set' => ['This test should be skipped in data set node inherited from abstract class'], + 'second_data_set' => ['This test should be skipped in data set node inherited from interface'], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/AddFixtureTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/AddFixtureTest.php index 67aaf3116f004..d351847ba4d1c 100644 --- a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/AddFixtureTest.php +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/AddFixtureTest.php @@ -31,6 +31,17 @@ protected function setUp(): void $this->config = $this->objectManager->get(ScopeConfigInterface::class); } + /** + * Checks that fixture added in global node successfully applied + * + * @return void + */ + public function testGloballyAddFixture(): void + { + $value = $this->config->getValue('test_section/test_group/field_4', ScopeInterface::SCOPE_STORES); + $this->assertEquals('4th field globally overridden value', $value); + } + /** * Checks that fixture added in test class node successfully applied * diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/ReplaceFixtureTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/ReplaceFixtureTest.php index 6e60d4cd90d97..9684f1754dad9 100644 --- a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/ReplaceFixtureTest.php +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/ReplaceFixtureTest.php @@ -31,6 +31,19 @@ protected function setUp(): void $this->config = $this->objectManager->get(ScopeConfigInterface::class); } + /** + * Checks that fixture can be replaced in global node + * + * @magentoConfigFixture current_store test_section/test_group/field_5 new_value + * + * @return void + */ + public function testGloballyReplaceFixture(): void + { + $value = $this->config->getValue('test_section/test_group/field_5', ScopeInterface::SCOPE_STORES); + $this->assertEquals('5th field globally replaced value', $value); + } + /** * Checks that fixture can be replaced in test class node * diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixture/SortFixturesTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixture/SortFixturesTest.php index 063a717a53669..62e9abcd96659 100644 --- a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixture/SortFixturesTest.php +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixture/SortFixturesTest.php @@ -27,6 +27,7 @@ protected function setUp(): void { parent::setUp(); + // phpstan:ignore "Class Magento\TestModuleOverrideConfig\Model\FixtureCallStorage not found." $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); } @@ -61,6 +62,7 @@ public function sortFixturesProvider(): array 'fixture2_first_module.php', 'fixture1_third_module.php', 'fixture3_first_module.php', + 'global_fixture_first_module.php',// globally added fixture 'fixture2_second_module.php', ], ], @@ -70,6 +72,7 @@ public function sortFixturesProvider(): array 'fixture1_second_module.php', 'fixture2_first_module.php', 'fixture3_first_module.php', + 'global_fixture_first_module.php',// globally added fixture 'fixture2_second_module.php', ], ], diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php new file mode 100644 index 0000000000000..2681d5b006e1c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** @var FixtureCallStorage $fixtureStorage */ +$fixtureStorage = Bootstrap::getObjectManager()->get(FixtureCallStorage::class); +$fixtureStorage->addFixtureToStorage(basename(__FILE__)); diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module_rollback.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module_rollback.php new file mode 100644 index 0000000000000..c2b0beacee170 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module_rollback.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** @var FixtureCallStorage $fixtureStorage */ +$fixtureStorage = Bootstrap::getObjectManager()->get(FixtureCallStorage::class); +$fixtureStorage->clearStorage(); diff --git a/dev/tests/integration/testsuite/Magento/Theme/Model/DesignTest.php b/dev/tests/integration/testsuite/Magento/Theme/Model/DesignTest.php index b3a47bc793a43..e1b645d0f1bbd 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Model/DesignTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Model/DesignTest.php @@ -49,6 +49,9 @@ public function testChangeDesign() $this->assertEquals('Magento/luma', $design->getDesignTheme()->getThemePath()); } + /** + * @magentoDbIsolation disabled + */ public function testCRUD() { $this->_model->setData( @@ -110,7 +113,7 @@ public function testLoadChangeCache() \Magento\Store\Model\StoreManagerInterface::class )->getDefaultStoreView()->getId(); // fixture design_change - + // phpcs:ignore Magento2.Security.InsecureFunction $cacheId = 'design_change_' . md5($storeId . $date); /** @var \Magento\Theme\Model\Design $design */ diff --git a/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/StoreThemesResolverInterfaceTest.php b/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/StoreThemesResolverInterfaceTest.php new file mode 100644 index 0000000000000..2f25d99bad6d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/StoreThemesResolverInterfaceTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; +use Magento\Theme\Model\ResourceModel\Theme\Collection; +use PHPUnit\Framework\TestCase; + +class StoreThemesResolverInterfaceTest extends TestCase +{ + const XML_PATH_THEME_USER_AGENT = 'design/theme/ua_regexp'; + /** + * @var StoreThemesResolverInterface + */ + private $model; + /** + * @var Collection + */ + private $themesCollection; + /** + * @var MutableScopeConfigInterface + */ + private $mutableScopeConfig; + /** + * @var Json + */ + private $serializer; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** + * @var string + */ + private $userAgentDesignConfig; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->model = $objectManager->get(StoreThemesResolverInterface::class); + $themesCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->themesCollection = $themesCollectionFactory->create(); + $this->mutableScopeConfig = $objectManager->get(MutableScopeConfigInterface::class); + $this->serializer = $objectManager->get(Json::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); + $scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $this->userAgentDesignConfig = $scopeConfig->getValue( + self::XML_PATH_THEME_USER_AGENT, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->mutableScopeConfig->setValue( + self::XML_PATH_THEME_USER_AGENT, + $this->userAgentDesignConfig, + ScopeInterface::SCOPE_STORE + ); + parent::tearDown(); + } + + /** + * @param array $config + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(array $config, array $expected): void + { + $store = $this->storeManager->getStore(); + $registeredThemes = []; + foreach ($this->themesCollection as $theme) { + $registeredThemes[$theme->getCode()] = $theme->getId(); + } + // convert themes code to id + foreach ($config as $key => $item) { + $config[$key]['value'] = $registeredThemes[$item['value']]; + } + $this->mutableScopeConfig->setValue( + self::XML_PATH_THEME_USER_AGENT, + $config ? $this->serializer->serialize($config) : null, + ScopeInterface::SCOPE_STORE, + $store->getCode() + ); + $expected = array_map( + function ($theme) use ($registeredThemes) { + return $registeredThemes[$theme]; + }, + $expected + ); + $this->assertEquals( + $expected, + $this->model->getThemes($store), + '', + 0.0, + 10, + true + ); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + [ + ], + [ + 'Magento/luma' + ] + ], + [ + [ + [ + 'search' => '\/Chrome\/i', + 'regexp' => '\/Chrome\/i', + 'value' => 'Magento/blank', + ] + ], + [ + 'Magento/luma', + 'Magento/blank' + ] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Translation/Controller/AjaxTest.php b/dev/tests/integration/testsuite/Magento/Translation/Controller/AjaxTest.php index 8c434e7e10cf7..c87278f230beb 100644 --- a/dev/tests/integration/testsuite/Magento/Translation/Controller/AjaxTest.php +++ b/dev/tests/integration/testsuite/Magento/Translation/Controller/AjaxTest.php @@ -12,6 +12,8 @@ /** * Test for Magento\Translation\Controller\Ajax class. + * + * @magentoDbIsolation disabled */ class AjaxTest extends \Magento\TestFramework\TestCase\AbstractController { diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js index 38728bca39192..a8ea949b52ebc 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js @@ -8,6 +8,7 @@ define([ 'jquery', 'moment', 'mageUtils', + 'mage/calendar', 'Magento_Ui/js/lib/knockout/bindings/datepicker' ], function (ko, $, moment, utils) { 'use strict'; @@ -18,6 +19,7 @@ define([ config; beforeEach(function () { + jasmine.clock().install(); element = $('<input />'); observable = ko.observable(); @@ -38,6 +40,7 @@ define([ }); afterEach(function () { + jasmine.clock().uninstall(); element.remove(); }); @@ -62,6 +65,8 @@ define([ expectedDate = moment(date, utils.convertToMomentFormat(inputFormat)).toDate(); observable(date); + jasmine.clock().tick(100); + expect(expectedDate.valueOf()).toEqual(element.datepicker('getDate').valueOf()); }); @@ -69,6 +74,8 @@ define([ element.datepicker('setTimezoneDate').blur().trigger('change'); observable(''); + jasmine.clock().tick(100); + expect(null).toEqual(element.datepicker('getDate')); }); }); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php index 7f7d9be162dec..d82c5e068f880 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php @@ -31,12 +31,12 @@ class ClassesTest extends \PHPUnit\Framework\TestCase /** * @var array */ - private static $keywordsBlacklist = ["String", "Array", "Boolean", "Element"]; + private static $excludeKeywords = ["String", "Array", "Boolean", "Element"]; /** * @var array|null */ - private $referenceBlackList = null; + private $excludeReference = null; /** * Set Up @@ -307,7 +307,7 @@ private function assertClassNamespace(string $file, string $relativePath, string public function testClassReferences() { $this->markTestSkipped("To be fixed in MC-33329. The test is not working properly " - . "after blacklisting logic was fixed. Previously it was ignoring all files."); + . "after excluded logic was fixed. Previously it was ignoring all files."); $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $invoker( /** @@ -373,7 +373,7 @@ function ($file) { ); $vendorClasses = array_filter($vendorClasses, 'strlen'); - $vendorClasses = $this->referenceBlacklistFilter($vendorClasses); + $vendorClasses = $this->excludedReferenceFilter($vendorClasses); if (!empty($vendorClasses)) { $this->assertClassesExist($vendorClasses, $file); } @@ -392,7 +392,7 @@ function ($file) { $badClasses = $this->handleAliasClasses($aliasClasses, $badClasses); } - $badClasses = $this->referenceBlacklistFilter($badClasses); + $badClasses = $this->excludedReferenceFilter($badClasses); $badClasses = $this->removeSpecialCases($badClasses, $file, $contents, $namespacePath); $this->assertClassReferences($badClasses, $file); }, @@ -426,12 +426,12 @@ private function handleAliasClasses(array $aliasClasses, array $badClasses): arr * @param array $classes * @return array */ - private function referenceBlacklistFilter(array $classes): array + private function excludedReferenceFilter(array $classes): array { - // exceptions made for the files from the blacklist - $blacklistClasses = $this->getReferenceBlacklist(); + // exceptions made for the files from the exclusion + $excludeClasses = $this->getExcludedReferences(); foreach ($classes as $class) { - if (in_array($class, $blacklistClasses)) { + if (in_array($class, $excludeClasses)) { unset($classes[array_search($class, $classes)]); } } @@ -444,16 +444,16 @@ private function referenceBlacklistFilter(array $classes): array * * @return array */ - private function getReferenceBlacklist(): array + private function getExcludedReferences(): array { - if (!isset($this->referenceBlackList)) { - $this->referenceBlackList = file( + if (!isset($this->excludeReference)) { + $this->excludeReference = file( __DIR__ . '/_files/blacklist/reference.txt', FILE_IGNORE_NEW_LINES ); } - return $this->referenceBlackList; + return $this->excludeReference; } /** @@ -479,7 +479,7 @@ private function removeSpecialCases(array $badClasses, string $file, string $con } // Remove usage of key words such as "Array", "String", and "Boolean" - if (in_array($badClass, self::$keywordsBlacklist)) { + if (in_array($badClass, self::$excludeKeywords)) { unset($badClasses[array_search($badClass, $badClasses)]); continue; } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/redundant_dependencies_webapi.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/redundant_dependencies_webapi.php new file mode 100644 index 0000000000000..41478ade9f901 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/redundant_dependencies_webapi.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'Magento\GraphQl' => [ + 'Magento\Webapi' => 'Magento\Webapi' + ] +]; diff --git a/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php b/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php index cd53516290252..d0c05613fbddd 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php +++ b/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php @@ -237,7 +237,7 @@ public function save($data, $id, $tags = [], $specificLifetime = false) $dataToSave = $data; $remHash = $this->loadRemoteDataVersion($id); - if ($remHash !== false) { + if ($remHash !== false && $this->getDataVersion($data) === $remHash) { $dataToSave = $this->remote->load($id); } else { $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php index bca23e0dcf31a..439648b3cc32b 100644 --- a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php +++ b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php @@ -116,7 +116,7 @@ public function lockedLoadData( callable $dataSaver ) { $cachedData = $dataLoader(); //optimistic read - $deadline = microtime(true) + $this->loadTimeout / 100; + $deadline = microtime(true) + $this->loadTimeout / 1000; if (empty($this->allowParallelGenerationConfigValue)) { $cacheConfig = $this diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php index bf936c9eb7994..ac6185262fb15 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php @@ -61,11 +61,13 @@ protected function setUp(): void } /** + * Test that exception is thrown if cache is not configured. + * * @param array $options * * @dataProvider initializeWithExceptionDataProvider */ - public function testInitializeWithException($options) + public function testInitializeWithException($options): void { $this->expectException('Zend_Cache_Exception'); $this->objectManager->getObject( @@ -79,7 +81,7 @@ public function testInitializeWithException($options) /** * @return array */ - public function initializeWithExceptionDataProvider() + public function initializeWithExceptionDataProvider(): array { return [ 'empty_backend_option' => [ @@ -104,11 +106,13 @@ public function initializeWithExceptionDataProvider() } /** + * Test that exception is not thrown if cache is configured. + * * @param array $options * * @dataProvider initializeWithOutExceptionDataProvider */ - public function testInitializeWithOutException($options) + public function testInitializeWithOutException($options): void { $result = $this->objectManager->getObject( RemoteSynchronizedCache::class, @@ -122,7 +126,7 @@ public function testInitializeWithOutException($options) /** * @return array */ - public function initializeWithOutExceptionDataProvider() + public function initializeWithOutExceptionDataProvider(): array { $connectionMock = $this->getMockBuilder(Mysql::class) ->disableOriginalConstructor() @@ -151,9 +155,11 @@ public function initializeWithOutExceptionDataProvider() } /** - * Test that load will always return newest data. + * Test that load will return the newest data. + * + * @return void */ - public function testLoadWithLocalData() + public function testLoad(): void { $localData = 1; $remoteData = 2; @@ -182,7 +188,12 @@ public function testLoadWithLocalData() $this->assertEquals($remoteData, $this->remoteSyncCacheInstance->load(1)); } - public function testLoadWithNoLocalAndNoRemoteData() + /** + * Test that load will not return data when no local data and no remote data exist. + * + * @return void + */ + public function testLoadWithNoLocalAndNoRemoteData(): void { $localData = false; $remoteData = false; @@ -197,10 +208,15 @@ public function testLoadWithNoLocalAndNoRemoteData() ->method('load') ->willReturn($remoteData); - $this->assertEquals($remoteData, $this->remoteSyncCacheInstance->load(1)); + $this->assertEquals(false, $this->remoteSyncCacheInstance->load(1)); } - public function testLoadWithNoLocalAndRemoteData() + /** + * Test that load will return the newest data when only remote data exists. + * + * @return void + */ + public function testLoadWithNoLocalAndWithRemoteData(): void { $localData = false; $remoteData = 1; @@ -223,7 +239,109 @@ public function testLoadWithNoLocalAndRemoteData() $this->assertEquals($remoteData, $this->remoteSyncCacheInstance->load(1)); } - public function testRemove() + /** + * Test that load will return the newest data when local data and remote data are the same. + * + * @return void + */ + public function testLoadWithEqualLocalAndRemoteData(): void + { + $localData = 1; + $remoteData = 1; + + $this->localCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn($localData); + + $this->remoteCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn(\hash('sha256', (string)$remoteData)); + + $this->assertEquals($localData, $this->remoteSyncCacheInstance->load(1)); + } + + /** + * Test that load will return stale cache. + * + * @return void + */ + public function testLoadWithStaleCache(): void + { + $localData = 1; + + $this->localCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn($localData); + + $this->remoteCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn(false); + + $closure = \Closure::bind(function ($cacheInstance) { + $cacheInstance->_options['use_stale_cache'] = true; + }, null, $this->remoteSyncCacheInstance); + $closure($this->remoteSyncCacheInstance); + + $this->remoteCacheMockExample + ->expects($this->at(2)) + ->method('load') + ->willReturn(true); + + $this->assertEquals($localData, $this->remoteSyncCacheInstance->load(1)); + } + + /** + * Test that load will generate data on the first attempt. + * + * @return void + */ + public function testLoadWithoutStaleCache(): void + { + $localData = 1; + + $this->localCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn($localData); + + $this->remoteCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn(false); + + $closure = \Closure::bind(function ($cacheInstance) { + $cacheInstance->_options['use_stale_cache'] = true; + }, null, $this->remoteSyncCacheInstance); + $closure($this->remoteSyncCacheInstance); + + $this->remoteCacheMockExample + ->expects($this->at(2)) + ->method('load') + ->willReturn(false); + + $closure = \Closure::bind(function ($cacheInstance) { + return $cacheInstance->lockSign; + }, null, $this->remoteSyncCacheInstance); + $lockSign = $closure($this->remoteSyncCacheInstance); + + $this->remoteCacheMockExample + ->expects($this->at(4)) + ->method('load') + ->willReturn($lockSign); + + $this->assertEquals(false, $this->remoteSyncCacheInstance->load(1)); + } + + /** + * Test data remove. + * + * @return void + */ + public function testRemove(): void { $this->remoteCacheMockExample ->expects($this->exactly(2)) @@ -238,7 +356,12 @@ public function testRemove() $this->remoteSyncCacheInstance->remove(1); } - public function testClean() + /** + * Test data clean. + * + * @return void + */ + public function testClean(): void { $this->remoteCacheMockExample ->expects($this->exactly(1)) @@ -248,7 +371,12 @@ public function testClean() $this->remoteSyncCacheInstance->clean(); } - public function testSaveWithRemoteData() + /** + * Test data save when remote data exist. + * + * @return void + */ + public function testSaveWithEqualRemoteData(): void { $remoteData = 1; @@ -270,7 +398,27 @@ public function testSaveWithRemoteData() $this->remoteSyncCacheInstance->save($remoteData, 1); } - public function testSaveWithoutRemoteData() + public function testSaveWithMismatchedRemoteData() + { + $remoteData = '1'; + + $this->remoteCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn(\hash('sha256', $remoteData)); + + $this->remoteCacheMockExample->expects($this->exactly(2))->method('save'); + $this->localCacheMockExample->expects($this->once())->method('save'); + + $this->remoteSyncCacheInstance->save(2, 1); + } + + /** + * Test data save when remote data is not exist. + * + * @return void + */ + public function testSaveWithoutRemoteData(): void { $this->remoteCacheMockExample ->expects($this->at(0)) diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php new file mode 100644 index 0000000000000..aa3df00953fda --- /dev/null +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php @@ -0,0 +1,181 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Cache\Test\Unit; + +use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\Lock\LockManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class LockGuardedCacheLoaderTest extends TestCase +{ + /** + * @var LockManagerInterface|MockObject + */ + private $lockManagerInterfaceMock; + + /** + * @var LockGuardedCacheLoader + */ + private $LockGuardedCacheLoader; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->lockManagerInterfaceMock = $this->getMockForAbstractClass(LockManagerInterface::class); + + $objectManager = new ObjectManagerHelper($this); + + $this->LockGuardedCacheLoader = $objectManager->getObject( + LockGuardedCacheLoader::class, + [ + 'locker' => $this->lockManagerInterfaceMock + ] + ); + } + + /** + * Verify optimistic data read from cache. + * + * @return void + */ + public function testOptimisticDataRead(): void + { + $lockName = \uniqid('lock_name_1_', true); + + $dataLoader = function () { + return 'loaded_data'; + }; + + $dataCollector = function () { + return true; + }; + + $dataSaver = function () { + return true; + }; + + $this->lockManagerInterfaceMock->expects($this->never())->method('lock'); + $this->lockManagerInterfaceMock->expects($this->never())->method('unlock'); + + $this->assertEquals( + 'loaded_data', + $this->LockGuardedCacheLoader->lockedLoadData($lockName, $dataLoader, $dataCollector, $dataSaver) + ); + } + + /** + * Verify data is collected when deadline to read from cache is reached. + * + * @return void + */ + public function testDataCollectedAfterDeadlineReached(): void + { + $lockName = \uniqid('lock_name_1_', true); + + $dataLoader = function () { + return false; + }; + + $dataCollector = function () { + return 'collected_data'; + }; + + $dataSaver = function () { + return true; + }; + + $this->lockManagerInterfaceMock + ->expects($this->atLeastOnce())->method('lock') + ->with($lockName, 10) + ->willReturn(false); + + $this->lockManagerInterfaceMock->expects($this->never())->method('unlock'); + + $this->assertEquals( + 'collected_data', + $this->LockGuardedCacheLoader->lockedLoadData($lockName, $dataLoader, $dataCollector, $dataSaver) + ); + } + + /** + * Verify data write to cache. + * + * @return void + */ + public function testDataWrite(): void + { + $lockName = \uniqid('lock_name_1_', true); + + $dataLoader = function () { + return false; + }; + + $dataCollector = function () { + return 'collected_data'; + }; + + $dataSaver = function () { + return true; + }; + + $this->lockManagerInterfaceMock + ->expects($this->once())->method('lock') + ->with($lockName, 10) + ->willReturn(true); + + $this->lockManagerInterfaceMock->expects($this->once())->method('unlock'); + + $this->assertEquals( + 'collected_data', + $this->LockGuardedCacheLoader->lockedLoadData($lockName, $dataLoader, $dataCollector, $dataSaver) + ); + } + + /** + * Verify data collected when Parallel Generation is allowed. + * + * @return void + */ + public function testDataCollectedWithParallelGeneration(): void + { + $lockName = \uniqid('lock_name_1_', true); + + $dataLoader = function () { + return false; + }; + + $dataCollector = function () { + return 'collected_data'; + }; + + $dataSaver = function () { + return true; + }; + + $closure = \Closure::bind(function ($cacheLoader) { + return $cacheLoader->allowParallelGenerationConfigValue = true; + }, null, $this->LockGuardedCacheLoader); + $closure($this->LockGuardedCacheLoader); + + $this->lockManagerInterfaceMock + ->expects($this->once())->method('lock') + ->with($lockName, 10) + ->willReturn(false); + + $this->lockManagerInterfaceMock->expects($this->never())->method('unlock'); + + $this->assertEquals( + 'collected_data', + $this->LockGuardedCacheLoader->lockedLoadData($lockName, $dataLoader, $dataCollector, $dataSaver) + ); + } +} diff --git a/lib/internal/Magento/Framework/Interception/ObjectManager/Config/Developer.php b/lib/internal/Magento/Framework/Interception/ObjectManager/Config/Developer.php index fac02b5d2614b..9d8d516b68fd4 100644 --- a/lib/internal/Magento/Framework/Interception/ObjectManager/Config/Developer.php +++ b/lib/internal/Magento/Framework/Interception/ObjectManager/Config/Developer.php @@ -58,8 +58,8 @@ public function setInterceptionConfig(\Magento\Framework\Interception\ConfigInte public function getInstanceType($instanceName) { $type = parent::getInstanceType($instanceName); - if ($this->interceptionConfig && $this->interceptionConfig->hasPlugins($instanceName) - && $this->interceptableValidator->validate($instanceName) + if ($this->interceptionConfig && $this->interceptionConfig->hasPlugins($type) + && $this->interceptableValidator->validate($type) ) { return $type . '\\Interceptor'; } diff --git a/lib/internal/Magento/Framework/Interception/Test/Unit/ObjectManager/Config/DeveloperTest.php b/lib/internal/Magento/Framework/Interception/Test/Unit/ObjectManager/Config/DeveloperTest.php index 75b520e94c70e..dd8dbe2f811dc 100644 --- a/lib/internal/Magento/Framework/Interception/Test/Unit/ObjectManager/Config/DeveloperTest.php +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/ObjectManager/Config/DeveloperTest.php @@ -58,4 +58,23 @@ public function testGetOriginalInstanceTypeReturnsInterceptedClass() $this->assertEquals('SomeClass\Interceptor', $this->model->getInstanceType('SomeClass')); $this->assertEquals('SomeClass', $this->model->getOriginalInstanceType('SomeClass')); } + + /** + * Test correct instance type is returned when plugins are created for virtual type parents + * + * @return void + */ + public function testGetInstanceTypeWithPluginOnVirtualTypeParent() : void + { + $reflectionClass = new \ReflectionClass(get_class($this->model)); + $reflectionProperty = $reflectionClass->getProperty('_virtualTypes'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->model, ['SomeVirtualClass' => 'SomeClass']); + + $this->interceptionConfig->expects($this->once())->method('hasPlugins')->with('SomeClass')->willReturn(true); + $this->model->setInterceptionConfig($this->interceptionConfig); + + $instanceType = $this->model->getInstanceType('SomeVirtualClass'); + $this->assertEquals('SomeClass\Interceptor', $instanceType); + } } diff --git a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php index a3e9ed61824ed..c4f6c67200b2b 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php @@ -44,7 +44,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getItems( BucketInterface $bucket, @@ -64,13 +64,12 @@ public function getItems( $aggregations['count'] ); - $this->algorithm->setLimits($aggregations['min'], $aggregations['max'] + 0.01); + $this->algorithm->setLimits($aggregations['min'], $aggregations['max']); $interval = $this->dataProvider->getInterval($bucket, $dimensions, $entityStorage); $data = $this->algorithm->calculateSeparators($interval); - $data[0]['from'] = ''; // We should not calculate min and max value - $data[count($data) - 1]['to'] = ''; + $data[0]['from'] = 0; $dataSize = count($data); for ($key = 0; $key < $dataSize; $key++) { diff --git a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php index 3bd0677c6a443..3929c4cb47573 100644 --- a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php +++ b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php @@ -92,9 +92,31 @@ private function getMca() */ public function isCurrent() { + $urlByPath = preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getPath())); return $this->getCurrent() || - preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getPath())) - == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getMca())); + ($urlByPath == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getMca()))) || + $this->isCurrentCmsUrl($urlByPath); + } + + /** + * Get Current displayed page url + * + * @return string + */ + private function getCurrentUrl() + { + return $this->getUrl('*/*/*', ['_current' => false, '_use_rewrite' => true]); + } + + /** + * Check if link URL equivalent to URL of currently displayed CMS page + * + * @param string $urlByPath + * @return bool + */ + private function isCurrentCmsUrl($urlByPath) + { + return ($urlByPath == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getCurrentUrl())); } /** diff --git a/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php b/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php index a585eda37df68..6efaadec4363c 100644 --- a/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php +++ b/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php @@ -5,11 +5,15 @@ */ namespace Magento\Framework\View\Layout; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\State; use Magento\Framework\View\Layout\Condition\ConditionFactory; +use Psr\Log\LoggerInterface; /** * Pool of generators for structural elements * @api + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GeneratorPool { @@ -24,31 +28,39 @@ class GeneratorPool protected $generators = []; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ protected $logger; /** - * @var \Magento\Framework\View\Layout\Condition\ConditionFactory + * @var ConditionFactory */ private $conditionFactory; + /** + * @var State + */ + private $state; + /** * @param ScheduledStructure\Helper $helper * @param ConditionFactory $conditionFactory - * @param \Psr\Log\LoggerInterface $logger + * @param LoggerInterface $logger * @param array|null $generators + * @param State|null $state */ public function __construct( ScheduledStructure\Helper $helper, ConditionFactory $conditionFactory, - \Psr\Log\LoggerInterface $logger, - array $generators = null + LoggerInterface $logger, + array $generators = null, + ?State $state = null ) { $this->helper = $helper; $this->conditionFactory = $conditionFactory; $this->logger = $logger; $this->addGenerators($generators); + $this->state = $state ?? ObjectManager::getInstance()->get(State::class); } /** @@ -226,7 +238,9 @@ protected function moveElementInStructure( $structure->setAsChild($element, $destination, $alias); $structure->reorderChildElement($destination, $element, $siblingName, $isAfter); } catch (\OutOfBoundsException $e) { - $this->logger->warning('Broken reference: ' . $e->getMessage()); + if ($this->state->getMode() === State::MODE_DEVELOPER) { + $this->logger->warning('Broken reference: ' . $e->getMessage()); + } } $scheduledStructure->unsetElementFromBrokenParentList($element); return $this; diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php index b6bf31f580ab6..852cb901e430e 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php @@ -14,109 +14,159 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @covers \Magento\Framework\View\Element\Html\Link\Current + */ class CurrentTest extends TestCase { /** - * @var MockObject + * @var UrlInterface|MockObject */ - protected $_urlBuilderMock; + private $_urlBuilderMock; /** - * @var MockObject + * @var Http|MockObject */ - protected $_requestMock; + private $_requestMock; /** - * @var ObjectManager + * @var Current */ - protected $_objectManager; + private $currentLink; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->_objectManager = new ObjectManager($this); - $this->_urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); + $this->_urlBuilderMock = $this->createMock(UrlInterface::class); $this->_requestMock = $this->createMock(Http::class); + + $this->currentLink = (new ObjectManager($this))->getObject( + Current::class, + [ + 'urlBuilder' => $this->_urlBuilderMock, + 'request' => $this->_requestMock + ] + ); } - public function testGetUrl() + /** + * Test get Url + */ + public function testGetUrl(): void { - $path = 'test/path'; - $url = 'http://example.com/asdasd'; + $pathStub = 'test/path'; + $urlStub = 'http://example.com/asdasd'; - $this->_urlBuilderMock->expects($this->once())->method('getUrl')->with($path)->willReturn($url); + $this->_urlBuilderMock->expects($this->once()) + ->method('getUrl') + ->with($pathStub) + ->will($this->returnValue($urlStub)); - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - ['urlBuilder' => $this->_urlBuilderMock] - ); + $this->currentLink->setPath($pathStub); - $link->setPath($path); - $this->assertEquals($url, $link->getHref()); + $this->assertEquals($urlStub, $this->currentLink->getHref()); } - public function testIsCurrentIfIsset() + /** + * Test if set current + */ + public function testIsCurrentIfIsset(): void { - /** @var Current $link */ - $link = $this->_objectManager->getObject(Current::class); - $link->setCurrent(true); - $this->assertTrue($link->isCurrent()); + $this->currentLink->setCurrent(true); + $this->assertTrue($this->currentLink->isCurrent()); } /** * Test if the current url is the same as link path * - * @return void + * @param string $pathStub + * @param string $urlStub + * @param array $request + * @param bool $expected + * @dataProvider isCurrentDataProvider */ - public function testIsCurrent() + public function testIsCurrent($pathStub, $urlStub, $request, $expected): void { - $path = 'test/index'; - $url = 'http://example.com/test/index'; - - $this->_requestMock->expects($this->once()) + $this->_requestMock->expects($this->any()) ->method('getPathInfo') - ->willReturn('/test/index/'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['pathInfoStub'])); + $this->_requestMock->expects($this->any()) ->method('getModuleName') - ->willReturn('test'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['moduleStub'])); + $this->_requestMock->expects($this->any()) ->method('getControllerName') - ->willReturn('index'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['controllerStub'])); + $this->_requestMock->expects($this->any()) ->method('getActionName') - ->willReturn('index'); + ->will($this->returnValue($request['actionStub'])); + $this->_urlBuilderMock->expects($this->at(0)) ->method('getUrl') - ->with($path) - ->willReturn($url); + ->with($pathStub) + ->will($this->returnValue($urlStub)); $this->_urlBuilderMock->expects($this->at(1)) ->method('getUrl') - ->with('test/index') - ->willReturn($url); - - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - [ - 'urlBuilder' => $this->_urlBuilderMock, - 'request' => $this->_requestMock - ] - ); - - $link->setPath($path); - $this->assertTrue($link->isCurrent()); + ->with($request['mcaStub']) + ->will($this->returnValue($request['getUrl'])); + + if ($request['mcaStub'] == '') { + $this->_urlBuilderMock->expects($this->at(2)) + ->method('getUrl') + ->with('*/*/*', ['_current' => false, '_use_rewrite' => true]) + ->will($this->returnValue($urlStub)); + } + + $this->currentLink->setPath($pathStub); + $this->assertEquals($expected, $this->currentLink->isCurrent()); } - public function testIsCurrentFalse() + /** + * Data provider for is current + */ + public function isCurrentDataProvider(): array { - $this->_urlBuilderMock->expects($this->at(0))->method('getUrl')->willReturn('1'); - $this->_urlBuilderMock->expects($this->at(1))->method('getUrl')->willReturn('2'); - - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - ['urlBuilder' => $this->_urlBuilderMock, 'request' => $this->_requestMock] - ); - $this->assertFalse($link->isCurrent()); + return [ + 'url with MCA' => [ + 'pathStub' => 'test/path', + 'urlStub' => 'http://example.com/asdasd', + 'requestStub' => [ + 'pathInfoStub' => '/test/index/', + 'moduleStub' => 'test', + 'controllerStub' => 'index', + 'actionStub' => 'index', + 'mcaStub' => 'test/index', + 'getUrl' => 'http://example.com/asdasd/' + ], + 'excepted' => true + ], + 'url with CMS' => [ + 'pathStub' => 'test', + 'urlStub' => 'http://example.com/test', + 'requestStub' => [ + 'pathInfoStub' => '//test//', + 'moduleStub' => 'cms', + 'controllerStub' => 'page', + 'actionStub' => 'view', + 'mcaStub' => '', + 'getUrl' => 'http://example.com/' + ], + 'excepted' => true + ], + 'Test if is current false' => [ + 'pathStub' => 'test/path', + 'urlStub' => 'http://example.com/tests', + 'requestStub' => [ + 'pathInfoStub' => '/test/index/', + 'moduleStub' => 'test', + 'controllerStub' => 'index', + 'actionStub' => 'index', + 'mcaStub' => 'test/index', + 'getUrl' => 'http://example.com/asdasd/' + ], + 'excepted' => false + ] + ]; } } diff --git a/lib/web/css/source/lib/_navigation.less b/lib/web/css/source/lib/_navigation.less index 38cd042591722..551e138ea06ec 100644 --- a/lib/web/css/source/lib/_navigation.less +++ b/lib/web/css/source/lib/_navigation.less @@ -470,6 +470,8 @@ li { margin: 0; + position: relative; + &.parent { > a { > .ui-menu-icon { diff --git a/lib/web/mage/adminhtml/browser.js b/lib/web/mage/adminhtml/browser.js index 604680e0bf8d5..74984024b74a0 100644 --- a/lib/web/mage/adminhtml/browser.js +++ b/lib/web/mage/adminhtml/browser.js @@ -376,7 +376,7 @@ define([ * @param {*} folderName */ confirm: function (folderName) { - return $.ajax({ + $.ajax({ url: self.options.newFolderUrl, dataType: 'json', data: { @@ -399,6 +399,8 @@ define([ ); } }, this)); + + return true; } } }); diff --git a/lib/web/mage/ie-class-fixer.js b/lib/web/mage/ie-class-fixer.js index 683090b1d1386..fe07f273a0b58 100644 --- a/lib/web/mage/ie-class-fixer.js +++ b/lib/web/mage/ie-class-fixer.js @@ -3,18 +3,10 @@ * See COPYING.txt for license details. */ -/* eslint-disable strict */ -(function () { - var userAgent = navigator.userAgent, // user agent identifier - html = document.documentElement, // html tag - gap = ''; // gap between classes +define([], function () { + 'use strict'; - if (html.className) { // check if neighbour class exist in html tag - gap = ' '; - } // end if - - if (userAgent.match(/Trident.*rv[ :]*11\./)) { // Special case for IE11 - html.className += gap + 'ie11'; - } // end if - -})(); + if (navigator.userAgent.match(/Trident.*rv[ :]*11\./)) { + document.documentElement.classList.add('ie11'); + } +}); diff --git a/lib/web/mage/touch-slider.js b/lib/web/mage/touch-slider.js new file mode 100644 index 0000000000000..6c468a832895b --- /dev/null +++ b/lib/web/mage/touch-slider.js @@ -0,0 +1,151 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'jquery-ui-modules/slider' +], function ($, _) { + 'use strict'; + + /** + * Adds support for touch events for regular jQuery UI slider. + */ + $.widget('mage.touchSlider', $.ui.slider, { + + /** + * Creates instance of widget. + * + * @override + */ + _create: function () { + _.bindAll( + this, + '_mouseDown', + '_mouseMove', + '_onTouchEnd' + ); + + return this._superApply(arguments); + }, + + /** + * Initializes mouse events on element. + * @override + */ + _mouseInit: function () { + var result = this._superApply(arguments); + + this.element + .off('mousedown.' + this.widgetName) + .on('touchstart.' + this.widgetName, this._mouseDown); + + return result; + }, + + /** + * Elements' 'mousedown' event handler polyfill. + * @override + */ + _mouseDown: function (event) { + var prevDelegate = this._mouseMoveDelegate, + result; + + event = this._touchToMouse(event); + result = this._super(event); + + if (prevDelegate === this._mouseMoveDelegate) { + return result; + } + + $(document) + .off('mousemove.' + this.widgetName) + .off('mouseup.' + this.widgetName); + + $(document) + .on('touchmove.' + this.widgetName, this._mouseMove) + .on('touchend.' + this.widgetName, this._onTouchEnd) + .on('tochleave.' + this.widgetName, this._onTouchEnd); + + return result; + }, + + /** + * Documents' 'mousemove' event handler polyfill. + * + * @override + * @param {Event} event - Touch event object. + */ + _mouseMove: function (event) { + event = this._touchToMouse(event); + + return this._super(event); + }, + + /** + * Documents' 'touchend' event handler. + */ + _onTouchEnd: function (event) { + $(document).trigger('mouseup'); + + return this._mouseUp(event); + }, + + /** + * Removes previously assigned touch handlers. + * + * @override + */ + _mouseUp: function () { + this._removeTouchHandlers(); + + return this._superApply(arguments); + }, + + /** + * Removes previously assigned touch handlers. + * + * @override + */ + _mouseDestroy: function () { + this._removeTouchHandlers(); + + return this._superApply(arguments); + }, + + /** + * Removes touch events from document object. + */ + _removeTouchHandlers: function () { + $(document) + .off('touchmove.' + this.widgetName) + .off('touchend.' + this.widgetName) + .off('touchleave.' + this.widgetName); + }, + + /** + * Adds properties to the touch event to mimic mouse event. + * + * @param {Event} event - Touch event object. + * @returns {Event} + */ + _touchToMouse: function (event) { + var orig = event.originalEvent, + touch = orig.touches[0]; + + return _.extend(event, { + which: 1, + pageX: touch.pageX, + pageY: touch.pageY, + clientX: touch.clientX, + clientY: touch.clientY, + screenX: touch.screenX, + screenY: touch.screenY + }); + } + }); + + return $.mage.touchSlider; +}); diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index 061416fcfd9ea..fe1cac3f076fd 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -46,6 +46,7 @@ use Magento\Setup\Module\SetupFactory; use Magento\Setup\Validator\DbValidator; use Magento\Store\Model\Store; +use Magento\Framework\App\Cache\Manager; /** * Class Installer contains the logic to install Magento application. @@ -1311,8 +1312,8 @@ public function uninstall() */ private function updateCaches($isEnabled, $types = []) { - /** @var \Magento\Framework\App\Cache\Manager $cacheManager */ - $cacheManager = $this->objectManagerProvider->get()->create(\Magento\Framework\App\Cache\Manager::class); + /** @var Manager $cacheManager */ + $cacheManager = $this->objectManagerProvider->get()->create(Manager::class); $availableTypes = $cacheManager->getAvailableTypes(); $types = empty($types) ? $availableTypes : array_intersect($availableTypes, $types); @@ -1331,8 +1332,9 @@ function (string $key) use ($types) { ); $this->log->log('Current status:'); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $this->log->log(print_r($cacheStatus, true)); + foreach ($cacheStatus as $cache => $status) { + $this->log->log(sprintf('%s: %d', $cache, $status)); + } } /** @@ -1344,8 +1346,8 @@ function (string $key) use ($types) { */ private function cleanCaches() { - /** @var \Magento\Framework\App\Cache\Manager $cacheManager */ - $cacheManager = $this->objectManagerProvider->get()->get(\Magento\Framework\App\Cache\Manager::class); + /** @var Manager $cacheManager */ + $cacheManager = $this->objectManagerProvider->get()->get(Manager::class); $types = $cacheManager->getAvailableTypes(); $cacheManager->clean($types); $this->log->log('Cache cleared successfully'); diff --git a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php index 0d12a73434355..8446486c2f104 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php @@ -450,12 +450,12 @@ public function installDataProvider() ['Installing user configuration...'], ['Enabling caches:'], ['Current status:'], - [print_r(['foo' => 1, 'bar' => 1], true)], + ['foo: 1'], + ['bar: 1'], ['Installing data...'], ['Data install/update:'], ['Disabling caches:'], ['Current status:'], - [print_r([], true)], ['Module \'Foo_One\':'], ['Module \'Bar_Two\':'], ['Data post-updates:'], @@ -463,7 +463,6 @@ public function installDataProvider() ['Module \'Bar_Two\':'], ['Enabling caches:'], ['Current status:'], - [print_r([], true)], ['Caches clearing:'], ['Cache cleared successfully'], ['Disabling Maintenance Mode:'], @@ -502,12 +501,12 @@ public function installDataProvider() ['Installing user configuration...'], ['Enabling caches:'], ['Current status:'], - [print_r(['foo' => 1, 'bar' => 1], true)], + ['foo: 1'], + ['bar: 1'], ['Installing data...'], ['Data install/update:'], ['Disabling caches:'], ['Current status:'], - [print_r([], true)], ['Module \'Foo_One\':'], ['Module \'Bar_Two\':'], ['Data post-updates:'], @@ -515,7 +514,6 @@ public function installDataProvider() ['Module \'Bar_Two\':'], ['Enabling caches:'], ['Current status:'], - [print_r([], true)], ['Installing admin user...'], ['Caches clearing:'], ['Cache cleared successfully'],