diff --git a/.github/ISSUE_TEMPLATE/story.md b/.github/ISSUE_TEMPLATE/story.md new file mode 100644 index 0000000000000..f4ba43d4c4389 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/story.md @@ -0,0 +1,14 @@ +--- +name: GraphQL Story +about: User story for GraphQL project +labels: 'Project: GraphQL' + +--- + +*As a ___ I want to ___ so that ___.* + +### AC +* a +* b +### Approved Schema +* a 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/ActionGroup/CliCacheCleanActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/CliCacheCleanActionGroup.xml new file mode 100644 index 0000000000000..c6305531e1c5d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/CliCacheCleanActionGroup.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="CliCacheCleanActionGroup"> + <annotations> + <description>Run cache:clean by CLI with specified cache tags (space separated).</description> + </annotations> + <arguments> + <argument name="tags" type="string"/> + </arguments> + + <magentoCLI command="cache:clean" arguments="{{tags}}" stepKey="cleanSpecifiedCache"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/CliCacheFlushActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/CliCacheFlushActionGroup.xml new file mode 100644 index 0000000000000..4dc18d1215139 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/CliCacheFlushActionGroup.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="CliCacheFlushActionGroup"> + <annotations> + <description>Run cache:flush by CLI with specified cache tags (space separated).</description> + </annotations> + <arguments> + <argument name="tags" type="string"/> + </arguments> + + <magentoCLI command="cache:flush" arguments="{{tags}}" stepKey="flushSpecifiedCache"/> + </actionGroup> +</actionGroups> 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/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml similarity index 100% rename from app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml rename to app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index a2fff5739f2f9..96d68d7e74117 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -377,8 +377,7 @@ private function prepareBundlePriceByType($priceType, array $dimensions, $entity ] ); - $query = $select->insertFromSelect($this->getBundlePriceTable()); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getBundlePriceTable(), []); } /** @@ -418,8 +417,7 @@ private function calculateBundleOptionPrice($priceTable, $dimensions) ] ); - $query = $select->insertFromSelect($this->getBundleOptionTable()); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleOptionTable(), []); $this->getConnection()->delete($priceTable->getTableName()); $this->applyBundlePrice($priceTable); @@ -575,8 +573,7 @@ private function calculateFixedBundleSelectionPrice() 'tier_price' => $tierExpr, ] ); - $query = $select->insertFromSelect($this->getBundleSelectionTable()); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleSelectionTable(), []); $this->applyFixedBundleSelectionPrice(); } @@ -627,8 +624,7 @@ private function calculateDynamicBundleSelectionPrice($dimensions) 'tier_price' => $tierExpr, ] ); - $query = $select->insertFromSelect($this->getBundleSelectionTable()); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleSelectionTable(), []); } /** @@ -697,8 +693,7 @@ private function prepareTierPriceIndex($dimensions, $entityIds) $select->where($this->dimensionToFieldMapper[$dimension->getName()] . ' = ?', $dimension->getValue()); } - $query = $select->insertFromSelect($this->getTable('catalog_product_index_tier_price')); - $connection->query($query); + $this->tableMaintainer->insertFromSelect($select, $this->getTable('catalog_product_index_tier_price'), []); } /** @@ -725,8 +720,7 @@ private function applyBundlePrice($priceTable): void ] ); - $query = $select->insertFromSelect($priceTable->getTableName()); - $this->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $priceTable->getTableName(), []); } /** @@ -785,7 +779,7 @@ private function getMainTable($dimensions) if ($this->fullReindexAction) { return $this->tableMaintainer->getMainReplicaTable($dimensions); } - return $this->tableMaintainer->getMainTable($dimensions); + return $this->tableMaintainer->getMainTableByDimensions($dimensions); } /** 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/AdminMassDeleteBundleProducts.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml similarity index 100% rename from app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml rename to app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml index caf500762883c..daa3351073e9b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml @@ -34,7 +34,7 @@ <requiredEntity createDataKey="createBundleOption"/> <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCLI stepKey="runCronIndex" command="cron:run --group=index"/> </before> <after> <!-- Delete Simple Product --> @@ -56,9 +56,11 @@ <actionGroup ref="AdminUpdateProductNameAndDescriptionAttributes" stepKey="updateProductAttribute"> <argument name="product" value="UpdateAttributeNameAndDescription"/> </actionGroup> - <!--Run cron twice--> - <magentoCLI command="cron:run" stepKey="cronRun"/> - <magentoCLI command="cron:run" stepKey="cronRunTwice"/> + <!-- Start message queue for product attribute consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminProductAttributeUpdateMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminProductAttributeUpdateMessageConsumerData.messageLimit}}"/> + </actionGroup> <!-- Search for a product with a new name and Open Product --> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="searchWithNewProductName"> <argument name="product" value="UpdateAttributeNameAndDescription"/> 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/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php index 76f5dbd1bea88..523efe08c6a4e 100644 --- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php +++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php @@ -149,7 +149,7 @@ public function getItems() $this->_compareProduct->setAllowUsedFlat(false); $this->_items = $this->_itemCollectionFactory->create(); - $this->_items->useProductItem(true)->setStoreId($this->_storeManager->getStore()->getId()); + $this->_items->useProductItem()->setStoreId($this->_storeManager->getStore()->getId()); if ($this->httpContext->getValue(Context::CONTEXT_AUTH)) { $this->_items->setCustomerId($this->currentCustomer->getCustomerId()); 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/Helper/Product/Compare.php b/app/code/Magento/Catalog/Helper/Product/Compare.php index 49a90c590a440..4e476fe8d1568 100644 --- a/app/code/Magento/Catalog/Helper/Product/Compare.php +++ b/app/code/Magento/Catalog/Helper/Product/Compare.php @@ -279,7 +279,7 @@ public function getItemCollection() // cannot be placed in constructor because of the cyclic dependency which cannot be fixed with proxy class // collection uses this helper in constructor when calling isEnabledFlat() method $this->_itemCollection = $this->_itemCollectionFactory->create(); - $this->_itemCollection->useProductItem(true)->setStoreId($this->_storeManager->getStore()->getId()); + $this->_itemCollection->useProductItem()->setStoreId($this->_storeManager->getStore()->getId()); if ($this->_customerSession->isLoggedIn()) { $this->_itemCollection->setCustomerId($this->_customerSession->getCustomerId()); @@ -313,7 +313,7 @@ public function calculate($logout = false) { /** @var $collection Collection */ $collection = $this->_itemCollectionFactory->create() - ->useProductItem(true); + ->useProductItem(); if (!$logout && $this->_customerSession->isLoggedIn()) { $collection->setCustomerId($this->_customerSession->getCustomerId()); } elseif ($this->_customerId) { 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/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php index e9a907f0b5097..f010536f06ee5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -173,7 +173,7 @@ protected function _syncData(array $processIds = []) } } - $query = $insertSelect->insertFromSelect($this->tableMaintainer->getMainTable($dimensions)); + $query = $insertSelect->insertFromSelect($this->tableMaintainer->getMainTableByDimensions($dimensions)); $this->getConnection()->query($query); } return $this; @@ -385,7 +385,7 @@ protected function _reindexRows($changedIds = []) // copy to index $this->_insertFromTable( $temporaryTable, - $this->tableMaintainer->getMainTable($dimensions) + $this->tableMaintainer->getMainTableByDimensions($dimensions) ); } } else { @@ -401,6 +401,8 @@ protected function _reindexRows($changedIds = []) } /** + * Delete Index data + * * @param array $entityIds * @return void */ @@ -408,7 +410,7 @@ private function deleteIndexData(array $entityIds) { foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $select = $this->getConnection()->select()->from( - ['index_price' => $this->tableMaintainer->getMainTable($dimensions)], + ['index_price' => $this->tableMaintainer->getMainTableByDimensions($dimensions)], null )->where('index_price.entity_id IN (?)', $entityIds); $query = $select->deleteFromSelect('index_price'); @@ -476,7 +478,7 @@ private function getIndexTargetTableByDimension(array $dimensions) { $indexTargetTable = $this->getIndexTargetTable(); if ($indexTargetTable === self::getIndexTargetTable()) { - $indexTargetTable = $this->tableMaintainer->getMainTable($dimensions); + $indexTargetTable = $this->tableMaintainer->getMainTableByDimensions($dimensions); } if ($indexTargetTable === self::getIndexTargetTable() . '_replica') { $indexTargetTable = $this->tableMaintainer->getMainReplicaTable($dimensions); @@ -497,6 +499,8 @@ protected function getIndexTargetTable() } /** + * Get product Id field name + * * @return string */ protected function getProductIdFieldName() @@ -533,6 +537,7 @@ private function getProductsTypes(array $changedIds = []) /** * Get parent products types + * * Used for add composite products to reindex if we have only simple products in changed ids set * * @param array $productsIds diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php index b30c85cfc52f0..390c9784b50de 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -425,7 +425,7 @@ private function switchTables(): void $mainTablesByDimension = []; foreach ($this->dimensionCollectionFactory->create() as $dimensions) { - $mainTablesByDimension[] = $this->dimensionTableMaintainer->getMainTable($dimensions); + $mainTablesByDimension[] = $this->dimensionTableMaintainer->getMainTableByDimensions($dimensions); //Move data from indexers with old realisation $this->moveDataFromReplicaTableToReplicaTables($dimensions); diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php index c418f2e1f253b..974bd0e8f7860 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php @@ -15,6 +15,8 @@ /** * Class to prepare new tables for new indexer mode + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ModeSwitcher implements \Magento\Indexer\Model\ModeSwitcherInterface { @@ -98,7 +100,6 @@ public function switchMode(string $currentMode, string $previousMode) * Create new tables * * @param string $currentMode - * * @return void * @throws \Zend_Db_Exception */ @@ -116,7 +117,6 @@ public function createTables(string $currentMode) * * @param string $currentMode * @param string $previousMode - * * @return void */ public function moveData(string $currentMode, string $previousMode) @@ -125,17 +125,17 @@ public function moveData(string $currentMode, string $previousMode) $dimensionsArrayForPreviousMode = $this->getDimensionsArray($previousMode); foreach ($dimensionsArrayForCurrentMode as $dimensionsForCurrentMode) { - $newTable = $this->tableMaintainer->getMainTable($dimensionsForCurrentMode); + $newTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForCurrentMode); if (empty($dimensionsForCurrentMode)) { // new mode is 'none' foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) { - $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode); + $oldTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForPreviousMode); $this->insertFromOldTablesToNew($newTable, $oldTable); } } else { // new mode is not 'none' foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) { - $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode); + $oldTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForPreviousMode); $this->insertFromOldTablesToNew($newTable, $oldTable, $dimensionsForCurrentMode); } } @@ -146,7 +146,6 @@ public function moveData(string $currentMode, string $previousMode) * Drop old tables * * @param string $previousMode - * * @return void */ public function dropTables(string $previousMode) @@ -164,7 +163,6 @@ public function dropTables(string $previousMode) * Get dimensions array * * @param string $mode - * * @return \Magento\Framework\Indexer\MultiDimensionProvider */ private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\MultiDimensionProvider @@ -184,7 +182,6 @@ private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\Mu * @param string $newTable * @param string $oldTable * @param Dimension[] $dimensions - * * @return void */ private function insertFromOldTablesToNew(string $newTable, string $oldTable, array $dimensions = []) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php index e3077baaeb7a6..5eed262352c76 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php @@ -7,36 +7,27 @@ namespace Magento\Catalog\Model\Indexer\Product\Price; -use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Indexer\Table\StrategyInterface; +use Magento\Framework\Model\ResourceModel\Db\Context as DbContext; use Magento\Framework\Search\Request\Dimension; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Search\Request\IndexScopeResolverInterface as TableResolver; +use Magento\Indexer\Model\ResourceModel\AbstractResource as AbstractIndexerResource; /** * Class encapsulate logic of work with tables per store in Product Price indexer */ -class TableMaintainer +class TableMaintainer extends AbstractIndexerResource { /** * Catalog product price index table name */ const MAIN_INDEX_TABLE = 'catalog_product_index_price'; - /** - * @var ResourceConnection - */ - private $resource; - /** * @var TableResolver */ private $tableResolver; - /** - * @var AdapterInterface - */ - private $connection; - /** * Catalog tmp category index table name */ @@ -53,47 +44,27 @@ class TableMaintainer private $mainTmpTable; /** - * @var null|string - */ - private $connectionName; - - /** - * @param ResourceConnection $resource + * @param DbContext $context + * @param StrategyInterface $tableStrategy * @param TableResolver $tableResolver - * @param null $connectionName + * @param string|null $connectionName */ public function __construct( - ResourceConnection $resource, + DbContext $context, + StrategyInterface $tableStrategy, TableResolver $tableResolver, $connectionName = null ) { - $this->resource = $resource; + parent::__construct($context, $tableStrategy, $connectionName); $this->tableResolver = $tableResolver; - $this->connectionName = $connectionName; - } - - /** - * Get connection for work with price indexer - * - * @return AdapterInterface - */ - public function getConnection(): AdapterInterface - { - if (null === $this->connection) { - $this->connection = $this->resource->getConnection($this->connectionName); - } - return $this->connection; } /** - * Return validated table name - * - * @param string $table - * @return string + * @inheritDoc */ - private function getTable(string $table): string + protected function _construct() { - return $this->resource->getTableName($table); + $this->_init(self::MAIN_INDEX_TABLE, 'entity_id'); } /** @@ -101,9 +72,7 @@ private function getTable(string $table): string * * @param string $mainTableName * @param string $newTableName - * * @return void - * * @throws \Zend_Db_Exception */ private function createTable(string $mainTableName, string $newTableName) @@ -119,7 +88,6 @@ private function createTable(string $mainTableName, string $newTableName) * Drop table * * @param string $tableName - * * @return void */ private function dropTable(string $tableName) @@ -133,7 +101,6 @@ private function dropTable(string $tableName) * Truncate table * * @param string $tableName - * * @return void */ private function truncateTable(string $tableName) @@ -147,7 +114,6 @@ private function truncateTable(string $tableName) * Get array key for tmp table * * @param Dimension[] $dimensions - * * @return string */ private function getArrayKeyForTmpTable(array $dimensions): string @@ -160,13 +126,12 @@ private function getArrayKeyForTmpTable(array $dimensions): string } /** - * Return main index table name + * Return main index table name using dimensions * * @param Dimension[] $dimensions - * * @return string */ - public function getMainTable(array $dimensions): string + public function getMainTableByDimensions(array $dimensions): string { return $this->tableResolver->resolve(self::MAIN_INDEX_TABLE, $dimensions); } @@ -175,14 +140,12 @@ public function getMainTable(array $dimensions): string * Create main and replica index tables for dimensions * * @param Dimension[] $dimensions - * * @return void - * * @throws \Zend_Db_Exception */ public function createTablesForDimensions(array $dimensions) { - $mainTableName = $this->getMainTable($dimensions); + $mainTableName = $this->getMainTableByDimensions($dimensions); //Create index table for dimensions based on main replica table //Using main replica table is necessary for backward capability and TableResolver plugin work $this->createTable( @@ -190,7 +153,7 @@ public function createTablesForDimensions(array $dimensions) $mainTableName ); - $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix; //Create replica table for dimensions based on main replica table $this->createTable( $this->getTable(self::MAIN_INDEX_TABLE . $this->additionalTableSuffix), @@ -202,15 +165,14 @@ public function createTablesForDimensions(array $dimensions) * Drop main and replica index tables for dimensions * * @param Dimension[] $dimensions - * * @return void */ public function dropTablesForDimensions(array $dimensions) { - $mainTableName = $this->getMainTable($dimensions); + $mainTableName = $this->getMainTableByDimensions($dimensions); $this->dropTable($mainTableName); - $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix; $this->dropTable($mainReplicaTableName); } @@ -218,15 +180,14 @@ public function dropTablesForDimensions(array $dimensions) * Truncate main and replica index tables for dimensions * * @param Dimension[] $dimensions - * * @return void */ public function truncateTablesForDimensions(array $dimensions) { - $mainTableName = $this->getMainTable($dimensions); + $mainTableName = $this->getMainTableByDimensions($dimensions); $this->truncateTable($mainTableName); - $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix; + $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix; $this->truncateTable($mainReplicaTableName); } @@ -234,26 +195,24 @@ public function truncateTablesForDimensions(array $dimensions) * Return replica index table name * * @param Dimension[] $dimensions - * * @return string */ public function getMainReplicaTable(array $dimensions): string { - return $this->getMainTable($dimensions) . $this->additionalTableSuffix; + return $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix; } /** * Create temporary index table for dimensions * * @param Dimension[] $dimensions - * * @return void */ public function createMainTmpTable(array $dimensions) { // Create temporary table based on template table catalog_product_index_price_tmp without indexes - $templateTableName = $this->resource->getTableName(self::MAIN_INDEX_TABLE . '_tmp'); - $temporaryTableName = $this->getMainTable($dimensions) . $this->tmpTableSuffix; + $templateTableName = $this->_resources->getTableName(self::MAIN_INDEX_TABLE . '_tmp'); + $temporaryTableName = $this->getMainTableByDimensions($dimensions) . $this->tmpTableSuffix; $this->getConnection()->createTemporaryTableLike($temporaryTableName, $templateTableName, true); $this->mainTmpTable[$this->getArrayKeyForTmpTable($dimensions)] = $temporaryTableName; } @@ -262,9 +221,7 @@ public function createMainTmpTable(array $dimensions) * Return temporary index table name * * @param Dimension[] $dimensions - * * @return string - * * @throws \LogicException */ public function getMainTmpTable(array $dimensions): string 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/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php index 5a055e5ed9603..47365929e159e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php @@ -62,7 +62,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function executeByDimensions(array $dimensions, \Traversable $entityIds) { @@ -81,8 +81,7 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) 'tierPriceField' => 'tier_price', ]); $select = $this->baseFinalPrice->getQuery($dimensions, $this->productType, iterator_to_array($entityIds)); - $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false); - $this->tableMaintainer->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []); $this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds)); } 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/AdminProductFormCategoryExistInCategoryListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryExistInCategoryListActionGroup.xml new file mode 100644 index 0000000000000..c9ad309dcadc1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryExistInCategoryListActionGroup.xml @@ -0,0 +1,26 @@ +<?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="AdminProductFormCategoryExistInCategoryListActionGroup"> + <annotations> + <description>Check Category exist in Category list for Assign to Product.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="{{categoryName}}" + stepKey="fillSearchCategory"/> + <see selector="{{AdminProductFormSection.selectCategory(categoryName)}}" userInput="{{categoryName}}" + stepKey="seeCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.xml new file mode 100644 index 0000000000000..fb0717fe173af --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.xml @@ -0,0 +1,26 @@ +<?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="AdminProductFormCategoryNotExistInCategoryListActionGroup"> + <annotations> + <description>Check Category not exist in Category list for Assign to Product.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="{{categoryName}}" + stepKey="fillSearchCategory"/> + <dontSee selector="{{AdminProductFormSection.selectCategory(categoryName)}}" userInput="{{categoryName}}" + stepKey="seeCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + </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/AdminSubmitCategoriesPopupActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml new file mode 100644 index 0000000000000..8905643658cd8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml @@ -0,0 +1,18 @@ +<?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="AdminSubmitCategoriesPopupActionGroup"> + <annotations> + <description>Clicks the "Done" button on the Search Categories popup.</description> + </annotations> + + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneButton" /> + </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/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml new file mode 100644 index 0000000000000..84e14269d24c2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.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="AssertStorefrontCategoryCurrentPageIsNthActionGroup"> + <arguments> + <argument name="expectedPage" type="string"/> + </arguments> + + <grabTextFrom selector="{{StorefrontCategoryBottomToolbarSection.currentPage}}" stepKey="currentPageText"/> + <assertEquals stepKey="assertIsPageNth"> + <expectedResult type="string">{{expectedPage}}</expectedResult> + <actualResult type="variable">currentPageText</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml index 22209b61d5316..43732c137c35c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml @@ -20,7 +20,9 @@ <waitForPageLoad stepKey="waitForCatalogSubmenu" time="5"/> <click stepKey="clickOnProducts" selector="{{CatalogSubmenuSection.products}}"/> <waitForPageLoad stepKey="waitForProductsPage" time="10"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> <click stepKey="TickCheckbox" selector="{{ProductsPageSection.checkboxForProduct(productName)}}"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click stepKey="OpenActions" selector="{{ProductsPageSection.actions}}"/> <waitForAjaxLoad stepKey="waitForDelete" time="5"/> <click stepKey="ChooseDelete" selector="{{ProductsPageSection.delete}}"/> 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/StorefrontNavigateCategoryNextPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml new file mode 100644 index 0000000000000..4776c9d32a34d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.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="StorefrontNavigateCategoryNextPageActionGroup"> + <annotations> + <description>Navigates storefront category next page from toolbar</description> + </annotations> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage"/> + <waitForPageLoad stepKey="waitForNextCategoryPageLoad"/> + </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/Data/QueueConsumerData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/QueueConsumerData.xml new file mode 100644 index 0000000000000..cd53fede3ab58 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/QueueConsumerData.xml @@ -0,0 +1,19 @@ +<?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="AdminProductAttributeUpdateConsumerData"> + <data key="consumerName">product_action_attribute.update</data> + <data key="messageLimit">100</data> + </entity> + <entity name="AdminProductAttributeWebsiteUpdateConsumerData"> + <data key="consumerName">product_action_attribute.website.update</data> + <data key="messageLimit">100</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/StorefrontCategoryBottomToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml index 09eb4ad954274..c27a6107e5e35 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml @@ -12,6 +12,6 @@ <element name="previousPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'previous')]" timeout="30"/> <element name="pageNumber" type="text" selector="//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> <element name="perPage" type="select" selector="//*[@class='toolbar toolbar-products'][2]//select[@id='limiter']"/> - <element name="currentPage" type="text" selector=".products.wrapper + .toolbar-products .pages .current span:nth-of-type(2)"/> + <element name="currentPage" type="text" selector=".//*[@class='toolbar toolbar-products'][2]//li[contains(@class, 'current')]//span[2]" timeout="30"/> </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/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml index 1c12b048e96d0..f8346f5a9dd5c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml @@ -38,8 +38,8 @@ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> + <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml index ff30c46a51c3a..0aa89bdfd45b6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -38,8 +38,8 @@ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> + <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml index 2b4437aed1bb2..171d15fe6ed4f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -38,8 +38,8 @@ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> + <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> 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/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml index e8e0d449aee4e..845ce340451d1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -56,11 +56,13 @@ <waitForPageLoad stepKey="waitForUpdatedProductToSave" /> <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitFormToReload1"/> + <!-- Start message queue --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueueConsumer"> + <argument name="consumerName" value="{{AdminProductAttributeUpdateConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminProductAttributeUpdateConsumerData.messageLimit}}"/> + </actionGroup> + <!-- Run cron --> + <magentoCLI command="cron:run --group=index" stepKey="runCron"/> <!--Verify product name, sku and updated price--> <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct1.sku$$)}}"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml index 31b5961edaaaa..cd34741b6a68c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml @@ -30,6 +30,7 @@ <createData entity="ApiSimpleProduct" stepKey="createProductTwo"> <requiredEntity createDataKey="createCategory"/> </createData> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </before> <after> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> @@ -39,6 +40,7 @@ <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAfterDelete"/> </after> <!-- Search and select products --> @@ -59,22 +61,23 @@ <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="AdminSwitchStoreViewActionGroup"/> <!-- Update attribute --> <checkOption selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="toggleToChangePrice"/> - <fillField selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="$$createProductOne.price$$0" stepKey="fillAttributeNameField"/> + <fillField selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="$createProductOne.price$0" stepKey="fillAttributeNameField"/> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" time="60" stepKey="waitForSuccessMessage"/> <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeAttributeUpdateSuccessMsg"/> - <!-- Run cron twice --> - <magentoCLI command="cron:run" arguments="--group=consumers" stepKey="runCron1"/> - <magentoCLI command="cron:run" arguments="--group=consumers" stepKey="runCron2"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Start message queue for product attribute consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminProductAttributeUpdateMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminProductAttributeUpdateMessageConsumerData.messageLimit}}"/> + </actionGroup> <!-- Assert on storefront default view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="searchByNameDefault"> - <argument name="name" value=""$$createProductOne.name$$""/> - <argument name="priceFrom" value="$$createProductOne.price$$0"/> - <argument name="priceTo" value="$$createProductOne.price$$0"/> + <argument name="name" value=""$createProductOne.name$""/> + <argument name="priceFrom" value="$createProductOne.price$0"/> + <argument name="priceTo" value="$createProductOne.price$0"/> </actionGroup> <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault"/> <waitForElementVisible selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="waitForSearchResultInDefaultView"/> @@ -84,9 +87,9 @@ <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupCustom"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="StorefrontSwitchStoreViewActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="searchByNameCustom"> - <argument name="name" value=""$$createProductOne.name$$""/> - <argument name="priceFrom" value="$$createProductOne.price$$0"/> - <argument name="priceTo" value="$$createProductOne.price$$0"/> + <argument name="name" value=""$createProductOne.name$""/> + <argument name="priceFrom" value="$createProductOne.price$0"/> + <argument name="priceTo" value="$createProductOne.price$0"/> </actionGroup> <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultCustom"/> <waitForElementVisible selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="waitForSearchResultInCustomView"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml index fe31456aca334..055f4e23cd9e7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml @@ -116,7 +116,7 @@ <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSuccessMessage"/> <!-- Run cron --> - <magentoCLI command="cron:run" stepKey="runCron"/> + <magentoCLI command="cron:run --group=index" stepKey="runCron"/> <!-- Clear invalidated cache on System>Tools>Cache Management page --> <amOnPage url="{{AdminCacheManagementPage.url}}" stepKey="onCachePage"/> @@ -188,7 +188,7 @@ <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSaveMessage"/> <!-- Run cron --> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <magentoCLI command="cron:run --group=index" stepKey="runCron2"/> <!-- Open frontend --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="onFrontendPage"/> 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/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml index 6ee1fd6a58e42..1214ba879f211 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -29,21 +29,17 @@ <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreViewFr"> <argument name="storeView" value="customStoreFR"/> </actionGroup> - <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <!-- Reindex invalidated indices and clear caches --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> - <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> - <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -52,6 +48,9 @@ <argument name="customStore" value="customStoreFR"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set catalog/frontend/flat_catalog_category 0 " stepKey="setFlatCatalogCategory"/> + <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setIndexersMode"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAgain"/> </after> <!--Verify Category is not listed in navigation menu--> <amOnPage url="/{{CatNotIncludeInMenu.name_lwr}}.html" stepKey="openCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml index dd79dd6824bbb..490f8dbdc4f81 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml @@ -30,16 +30,17 @@ <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreViewFr"> <argument name="storeView" value="customStoreFR"/> </actionGroup> - <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <!--Run full reindex and clear caches --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml similarity index 100% rename from app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml rename to app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml 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/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml index 677752be02eb5..785d19c000af0 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml @@ -79,8 +79,6 @@ <requiredEntity createDataKey="createBundleOptionWithAttribute"/> <requiredEntity createDataKey="secondSimpleProductForFixedWithAttribute"/> </createData> - - <magentoCron stepKey="runCron"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -94,9 +92,8 @@ <deleteData createDataKey="firstSimpleProductForFixedWithAttribute" stepKey="deleteFirstSimpleProductForFixedWithAttribute"/> <deleteData createDataKey="secondSimpleProductForFixedWithAttribute" stepKey="deleteSecondSimpleProductForFixedWithAttribute"/> <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> - - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Go to export page --> @@ -105,7 +102,12 @@ <!-- Export created below products --> <actionGroup ref="ExportAllProductsActionGroup" stepKey="exportCreatedProducts"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> + </actionGroup> + <reloadPage stepKey="refreshPage"/> <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml index 5078fa5c571db..9f8d65968d741 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml @@ -48,7 +48,6 @@ <requiredEntity createDataKey="createSecondSimpleProduct"/> </updateData> - <magentoCron stepKey="runCron"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -56,7 +55,7 @@ <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -68,8 +67,13 @@ <!-- Export created below products --> <actionGroup ref="ExportAllProductsActionGroup" stepKey="exportCreatedProducts"/> - <magentoCron stepKey="runCronIndex" groups="index"/> - + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> + </actionGroup> + <reloadPage stepKey="refreshPage"/> + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml index 8eba6a39f6199..d2517fa28cdd1 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml @@ -150,9 +150,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Admin logout--> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> <!-- Go to System > Export --> @@ -164,10 +164,13 @@ <argument name="attributeData" value="$$createExportImportConfigurableProduct.sku$$"/> </actionGroup> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCronFirstTime"/> - <magentoCLI command="cron:run" stepKey="runCronSecondTime"/> - + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> + </actionGroup> + <reloadPage stepKey="refreshPage"/> + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Save exported file: file successfully downloaded --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml index 44f7b91324025..94478e63aa92a 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml @@ -83,9 +83,8 @@ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Go to export page --> @@ -98,8 +97,13 @@ <argument name="attributeData" value="$$createConfigProduct.sku$$"/> </actionGroup> - <magentoCron stepKey="runCron"/> - + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> + </actionGroup> + <reloadPage stepKey="refreshPage"/> + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml index 744e51bfe8896..95cfe2c87bffb 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml @@ -89,7 +89,7 @@ <requiredEntity createDataKey="createConfigProduct"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCron groups="index" stepKey="runCronIndex"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -99,9 +99,8 @@ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Go to export page --> @@ -113,8 +112,13 @@ <argument name="attributeData" value="$$createConfigProduct.sku$$"/> </actionGroup> - <magentoCron stepKey="runCron"/> - + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> + </actionGroup> + <reloadPage stepKey="refreshPage"/> + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml index 09f37a10fb14d..2f57d94113d38 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml @@ -85,9 +85,8 @@ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Go to export page --> @@ -97,8 +96,13 @@ <!-- Export created below products --> <actionGroup ref="ExportAllProductsActionGroup" stepKey="exportCreatedProducts"/> - <magentoCron stepKey="runCron"/> - + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> + </actionGroup> + <reloadPage stepKey="refreshPage"/> + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml index d345b0b0de116..dac97a61a967b 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml @@ -28,7 +28,8 @@ <requiredEntity createDataKey="createAttributeSet"/> </createData> - <magentoCron stepKey="runCron"/> + <magentoCLI command="cron:run" arguments="--group index" stepKey="cronRun"/> + <magentoCLI command="cron:run" arguments="--group index" stepKey="cronRunToStartReindex"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -36,7 +37,7 @@ <deleteData createDataKey="createSimpleProductWithCustomAttributeSet" stepKey="deleteSimpleProductWithCustomAttributeSet"/> <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -48,12 +49,13 @@ <!-- Export created below products --> <actionGroup ref="ExportAllProductsActionGroup" stepKey="exportCreatedProducts"/> - <magentoCron stepKey="runCronIndex" groups="index"/> - - <wait stepKey="waitForReindexing" time="60" /> + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminExportMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminExportMessageConsumerData.messageLimit}}"/> + </actionGroup> <reloadPage stepKey="pageReload" /> - <waitForPageLoad stepKey="waitForPageLoaded" /> - + <waitForElementVisible selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="waitForFileName"/> <grabTextFrom selector="{{AdminExportAttributeSection.exportFileNameByPosition('0')}}" stepKey="grabNameFile"/> <!-- Download product --> diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php b/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php deleted file mode 100644 index c061c459bfb49..0000000000000 --- a/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogInventory\Model\Plugin; - -use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; -use Magento\Catalog\Model\Indexer\Product\Price\Processor; -use Magento\Framework\Model\AbstractModel; - -/** - * Update product price index after product stock status changed. - */ -class PriceIndexUpdater -{ - /** - * @var Processor - */ - private $priceIndexProcessor; - - /** - * @param Processor $priceIndexProcessor - */ - public function __construct(Processor $priceIndexProcessor) - { - $this->priceIndexProcessor = $priceIndexProcessor; - } - - /** - * @param Item $subject - * @param Item $result - * @param AbstractModel $model - * @return Item - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterSave(Item $subject, Item $result, AbstractModel $model): Item - { - $fields = [ - 'is_in_stock', - 'use_config_manage_stock', - 'manage_stock', - ]; - foreach ($fields as $field) { - if ($model->dataHasChangedFor($field)) { - $this->priceIndexProcessor->reindexRow($model->getProductId()); - break; - } - } - - return $result; - } - - /** - * @param Item $subject - * @param mixed $result - * @param int $websiteId - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterUpdateSetOutOfStock(Item $subject, $result, int $websiteId) - { - $this->priceIndexProcessor->markIndexerAsInvalid(); - } - - /** - * @param Item $subject - * @param mixed $result - * @param int $websiteId - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterUpdateSetInStock(Item $subject, $result, int $websiteId) - { - $this->priceIndexProcessor->markIndexerAsInvalid(); - } -} diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php index edccad60231ec..dc9233e77d3a9 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php @@ -5,14 +5,13 @@ */ namespace Magento\CatalogInventory\Model\ResourceModel\Stock; -use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexProcessor; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\Stock; use Magento\CatalogInventory\Model\Indexer\Stock\Processor; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\DB\Select; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\DateTime\DateTime; /** @@ -42,27 +41,33 @@ class Item extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ private $dateTime; + /** + * @var PriceIndexProcessor + */ + private $priceIndexProcessor; + /** * @param Context $context * @param Processor $processor - * @param string $connectionName * @param StockConfigurationInterface $stockConfiguration * @param DateTime $dateTime + * @param PriceIndexProcessor $priceIndexProcessor + * @param string $connectionName */ public function __construct( Context $context, Processor $processor, - $connectionName = null, - StockConfigurationInterface $stockConfiguration = null, - DateTime $dateTime = null + StockConfigurationInterface $stockConfiguration, + DateTime $dateTime, + PriceIndexProcessor $priceIndexProcessor, + $connectionName = null ) { $this->stockIndexerProcessor = $processor; parent::__construct($context, $connectionName); - $this->stockConfiguration = $stockConfiguration ?? - ObjectManager::getInstance()->get(StockConfigurationInterface::class); - $this->dateTime = $dateTime ?? - ObjectManager::getInstance()->get(DateTime::class); + $this->stockConfiguration = $stockConfiguration; + $this->dateTime = $dateTime; + $this->priceIndexProcessor = $priceIndexProcessor; } /** @@ -144,10 +149,25 @@ protected function _prepareDataForTable(\Magento\Framework\DataObject $object, $ protected function _afterSave(AbstractModel $object) { parent::_afterSave($object); - /** @var StockItemInterface $object */ + + $productId = $object->getProductId(); if ($this->processIndexEvents) { - $this->stockIndexerProcessor->reindexRow($object->getProductId()); + $this->stockIndexerProcessor->reindexRow($productId); } + $fields = [ + 'is_in_stock', + 'use_config_manage_stock', + 'manage_stock', + ]; + foreach ($fields as $field) { + if ($object->dataHasChangedFor($field)) { + $this->addCommitCallback(function () use ($productId) { + $this->priceIndexProcessor->reindexRow($productId); + }); + break; + } + } + return $this; } @@ -196,6 +216,7 @@ public function updateSetOutOfStock(int $websiteId) $connection->update($this->getMainTable(), $values, $where); $this->stockIndexerProcessor->markIndexerAsInvalid(); + $this->priceIndexProcessor->markIndexerAsInvalid(); } /** @@ -228,6 +249,7 @@ public function updateSetInStock(int $websiteId) $connection->update($this->getMainTable(), $values, $where); $this->stockIndexerProcessor->markIndexerAsInvalid(); + $this->priceIndexProcessor->markIndexerAsInvalid(); } /** diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index 78a0c2b734315..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> @@ -129,9 +129,6 @@ </argument> </arguments> </type> - <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item"> - <plugin name="priceIndexUpdater" type="Magento\CatalogInventory\Model\Plugin\PriceIndexUpdater" /> - </type> <type name="Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save"> <plugin name="massAction" type="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute" /> </type> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml index b9318f72bee9e..c3132e5c46cc9 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-71"/> <group value="CatalogRule"/> </annotations> + <before> <!-- Create a simple product and a category--> <createData entity="ApiCategory" stepKey="createCategory"/> @@ -25,33 +26,32 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> + <!-- Perform reindex and flush cache --> + <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> </before> + <after> <!-- Delete the simple product and category --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <!-- Delete the catalog rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToRulePage"/> - <waitForPageLoad stepKey="waitForRulePage"/> - <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> - <argument name="name" value="{{_defaultCatalogRule.name}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> - </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> <!-- Create a catalog rule for the NOT LOGGED IN customer group --> - <actionGroup ref="NewCatalogPriceRuleByUIActionGroup" stepKey="createNewPriceRule"/> - <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> - <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> - <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> - - <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="createNewPriceRule"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForPriceRule"> + <argument name="groups" value="'NOT LOGGED IN'"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsPriceRule"/> + <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyFPriceRule"/> <!-- As a NOT LOGGED IN user, go to the storefront category page and should see the discount --> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategory1"/> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToCategory1"> + <argument name="category" value="$createCategory$"/> + </actionGroup> <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createProduct.name$$" stepKey="seeProduct1"/> <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$110.70" stepKey="seeDiscountedPrice1"/> @@ -67,7 +67,9 @@ </actionGroup> <!-- As a logged in user, go to the storefront category page and should NOT see discount --> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategory2"/> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToCategory2"> + <argument name="category" value="$createCategory$"/> + </actionGroup> <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createProduct.name$$" stepKey="seeProduct2"/> <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$123.00" stepKey="seeDiscountedPrice2"/> </test> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml index 6c436fee808a7..9d7607d7521c9 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml @@ -44,7 +44,7 @@ <createData entity="productDropDownAttribute" stepKey="createSecondProductAttribute"> <field key="scope">website</field> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCLI command="cron:run --group=index" stepKey="runCron"/> </before> <after> @@ -64,7 +64,7 @@ <deleteData createDataKey="createSecondProductAttribute" stepKey="deleteSecondProductAttribute"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCLI command="cron:run --group=index" stepKey="runCron"/> </after> <!--Create catalog price rule--> @@ -80,7 +80,6 @@ <argument name="targetSelectValue" value="is undefined"/> </actionGroup> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> - <wait time="60" stepKey="waitMinute"/> <magentoCLI command="indexer:reindex" stepKey="reindex"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> <magentoCLI command="cache:flush" stepKey="flushCache3"/> @@ -129,7 +128,6 @@ <argument name="targetSelectValue" value="is undefined"/> </actionGroup> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules1"/> - <wait time="60" stepKey="waitForMinute"/> <magentoCLI command="indexer:reindex" stepKey="reindex1"/> <magentoCLI command="cache:flush" stepKey="flushCache1"/> <magentoCLI command="cache:flush" stepKey="flushCache2"/> diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index 28624c667e42b..360df8f4edc66 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -572,11 +572,11 @@ public function prepareProductIndex($indexData, $productData, $storeId) foreach ($indexData as $entityId => $attributeData) { foreach ($attributeData as $attributeId => $attributeValues) { $value = $this->getAttributeValue($attributeId, $attributeValues, $storeId); - if (!empty($value)) { + if ($value !== null && $value !== false && $value != '') { if (!isset($index[$attributeId])) { $index[$attributeId] = []; } - $index[$attributeId][$entityId] = $value; + $index[$attributeId][$entityId] = $value; } } } 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/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml index 67e9fdd43f5fe..cceac0475aa78 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml @@ -13,6 +13,7 @@ <stories value="Use Advanced Search"/> <title value="Unable negative price use to advanced search"/> <description value="Check unable negative price use to advanced search by price from and price to"/> + <severity value="MAJOR"/> </annotations> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="openAdvancedSearch"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php index f8bf00b16cf37..31bb630718e55 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php @@ -3,14 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogUrlRewrite\Model\Category\Plugin\Store; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryFactory; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\Framework\Model\AbstractModel; +use Magento\Store\Model\ResourceModel\Store; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; @@ -19,32 +23,31 @@ * * @see \Magento\Store\Model\ResourceModel\Store * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @package Magento\CatalogUrlRewrite\Model\Category\Plugin\Store */ class View { /** - * @var \Magento\UrlRewrite\Model\UrlPersistInterface + * @var UrlPersistInterface */ protected $urlPersist; /** - * @var \Magento\Catalog\Model\CategoryFactory + * @var CategoryFactory */ protected $categoryFactory; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $productFactory; /** - * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator + * @var CategoryUrlRewriteGenerator */ protected $categoryUrlRewriteGenerator; /** - * @var \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator + * @var ProductUrlRewriteGenerator */ protected $productUrlRewriteGenerator; @@ -75,114 +78,108 @@ public function __construct( } /** - * @param \Magento\Store\Model\ResourceModel\Store $object + * Setter for Orig Store data + * + * @param Store $object * @param AbstractModel $store * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeSave( - \Magento\Store\Model\ResourceModel\Store $object, + Store $object, AbstractModel $store - ) { + ): void { $this->origStore = $store; } /** * Regenerate urls on store after save * - * @param \Magento\Store\Model\ResourceModel\Store $object - * @param \Magento\Store\Model\ResourceModel\Store $store - * @return \Magento\Store\Model\ResourceModel\Store + * @param Store $object + * @param Store $store + * @return Store * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterSave( - \Magento\Store\Model\ResourceModel\Store $object, - \Magento\Store\Model\ResourceModel\Store $store - ) { + Store $object, + Store $store + ): Store { if ($this->origStore->isObjectNew() || $this->origStore->dataHasChangedFor('group_id')) { - if (!$this->origStore->isObjectNew()) { - $this->urlPersist->deleteByData([UrlRewrite::STORE_ID => $this->origStore->getId()]); - } - - $this->urlPersist->replace( - $this->generateCategoryUrls($this->origStore->getRootCategoryId(), $this->origStore->getId()) + $categoryRewriteUrls = $this->generateCategoryUrls( + (int)$this->origStore->getRootCategoryId(), + (int)$this->origStore->getId() ); + $this->urlPersist->replace($categoryRewriteUrls); + $this->urlPersist->replace( - $this->generateProductUrls( - $this->origStore->getWebsiteId(), - $this->origStore->getOrigData('website_id'), - $this->origStore->getId() - ) + $this->generateProductUrls((int)$this->origStore->getId()) ); } + return $store; } /** - * Generate url rewrites for products assigned to website + * Generate url rewrites for products assigned to store * - * @param int $websiteId - * @param int $originWebsiteId * @param int $storeId * @return array */ - protected function generateProductUrls($websiteId, $originWebsiteId, $storeId) + protected function generateProductUrls(int $storeId): array { $urls = []; - $websiteIds = $websiteId != $originWebsiteId && $originWebsiteId !== null - ? [$websiteId, $originWebsiteId] - : [$websiteId]; $collection = $this->productFactory->create() ->getCollection() ->addCategoryIds() ->addAttributeToSelect(['name', 'url_path', 'url_key', 'visibility']) - ->addWebsiteFilter($websiteIds); + ->addStoreFilter($storeId); foreach ($collection as $product) { + /** @var Product $product */ $product->setStoreId($storeId); - /** @var \Magento\Catalog\Model\Product $product */ - $urls = array_merge( - $urls, - $this->productUrlRewriteGenerator->generate($product) - ); + $urls[] = $this->productUrlRewriteGenerator->generate($product); } + $urls = array_merge([], ...$urls); + return $urls; } /** + * Generate url rewrites for categories assigned to store + * * @param int $rootCategoryId * @param int $storeId * @return array */ - protected function generateCategoryUrls($rootCategoryId, $storeId) + protected function generateCategoryUrls(int $rootCategoryId, int $storeId): array { $urls = []; - $categories = $this->categoryFactory->create()->getCategories($rootCategoryId, 1, false, true); + $categories = $this->categoryFactory->create()->getCategories($rootCategoryId, 1, false, true, false); + $categories->setStoreId($storeId); foreach ($categories as $category) { - /** @var \Magento\Catalog\Model\Category $category */ + /** @var Category $category */ $category->setStoreId($storeId); - $urls = array_merge( - $urls, - $this->categoryUrlRewriteGenerator->generate($category) - ); + $urls[] = $this->categoryUrlRewriteGenerator->generate($category); } + $urls = array_merge([], ...$urls); + return $urls; } /** * Delete unused url rewrites * - * @param \Magento\Store\Model\ResourceModel\Store $subject - * @param \Magento\Store\Model\ResourceModel\Store $result + * @param Store $subject + * @param Store $result * @param AbstractModel $store - * @return \Magento\Store\Model\ResourceModel\Store + * @return Store * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterDelete( - \Magento\Store\Model\ResourceModel\Store $subject, - \Magento\Store\Model\ResourceModel\Store $result, + Store $subject, + Store $result, AbstractModel $store - ) { + ): Store { $this->urlPersist->deleteByData([UrlRewrite::STORE_ID => $store->getId()]); return $result; diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml index 75ae9d821c356..329f5e8cae3f6 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -72,11 +72,13 @@ <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitFormToReload1"/> + <!-- Start message queue --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueueConsumer"> + <argument name="consumerName" value="{{AdminProductAttributeWebsiteUpdateConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminProductAttributeWebsiteUpdateConsumerData.messageLimit}}"/> + </actionGroup> + <!-- Run cron --> + <magentoCLI command="cron:run --group=index" stepKey="runCron"/> <!--Got to Store front product page and check url--> <amOnPage url="{{StorefrontProductPage.url($$createProduct.sku$$-new)}}" stepKey="navigateToSimpleProductPage"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php index 4471ea0e59887..61557db883aa1 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php @@ -7,6 +7,7 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category\Plugin\Store; +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryFactory; use Magento\Catalog\Model\Product; @@ -87,6 +88,9 @@ class ViewTest extends TestCase */ private $productMock; + /** + * @inheritdoc + */ protected function setUp(): void { $this->objectManager = new ObjectManager($this); @@ -121,7 +125,7 @@ protected function setUp(): void ->getMock(); $this->productCollectionMock = $this->getMockBuilder(ProductCollection::class) ->disableOriginalConstructor() - ->setMethods(['addCategoryIds', 'addAttributeToSelect', 'addWebsiteFilter', 'getIterator']) + ->setMethods(['addCategoryIds', 'addAttributeToSelect', 'getIterator', 'addStoreFilter']) ->getMock(); $this->productMock = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() @@ -139,7 +143,12 @@ protected function setUp(): void ); } - public function testAfterSave() + /** + * Test after save + * + * @return void + */ + public function testAfterSave(): void { $origStoreMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() @@ -155,9 +164,16 @@ public function testAfterSave() $this->abstractModelMock->expects($this->any()) ->method('isObjectNew') ->willReturn(true); + $categoryCollection = $this->getMockBuilder(CategoryCollection::class) + ->disableOriginalConstructor() + ->setMethods(['getIterator']) + ->getMock(); + $categoryCollection->expects($this->any()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([])); $this->categoryMock->expects($this->once()) ->method('getCategories') - ->willReturn([]); + ->willReturn($categoryCollection); $this->categoryFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->categoryMock); @@ -174,7 +190,7 @@ public function testAfterSave() ->method('addAttributeToSelect') ->willReturn($this->productCollectionMock); $this->productCollectionMock->expects($this->once()) - ->method('addWebsiteFilter') + ->method('addStoreFilter') ->willReturn($this->productCollectionMock); $iterator = new \ArrayIterator([$this->productMock]); $this->productCollectionMock->expects($this->once()) @@ -191,7 +207,12 @@ public function testAfterSave() ); } - public function testAfterDelete() + /** + * Test after delete + * + * @return void + */ + public function testAfterDelete(): void { $this->urlPersistMock->expects($this->once()) ->method('deleteByData'); diff --git a/app/code/Magento/Checkout/Controller/Cart/Add.php b/app/code/Magento/Checkout/Controller/Cart/Add.php index 739f71caeb804..c4294e0fbade1 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Add.php +++ b/app/code/Magento/Checkout/Controller/Cart/Add.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Checkout\Model\Cart as CustomerCart; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\Exception\NoSuchEntityException; /** @@ -77,7 +79,7 @@ protected function _initProduct() /** * Add product to shopping cart action * - * @return \Magento\Framework\Controller\Result\Redirect + * @return ResponseInterface|ResultInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() @@ -90,7 +92,6 @@ public function execute() } $params = $this->getRequest()->getParams(); - try { if (isset($params['qty'])) { $filter = new \Zend_Filter_LocalizedToNormalized( @@ -104,9 +105,7 @@ public function execute() $product = $this->_initProduct(); $related = $this->getRequest()->getParam('related_product'); - /** - * Check product availability - */ + /** Check product availability */ if (!$product) { return $this->goBack(); } @@ -115,7 +114,6 @@ public function execute() if (!empty($related)) { $this->cart->addProductsByIds(explode(',', $related)); } - $this->cart->save(); /** @@ -127,21 +125,25 @@ public function execute() ); if (!$this->_checkoutSession->getNoCartRedirect(true)) { - if (!$this->cart->getQuote()->getHasError()) { - if ($this->shouldRedirectToCart()) { - $message = __( - 'You added %1 to your shopping cart.', - $product->getName() - ); - $this->messageManager->addSuccessMessage($message); - } else { - $this->messageManager->addComplexSuccessMessage( - 'addCartSuccessMessage', - [ - 'product_name' => $product->getName(), - 'cart_url' => $this->getCartUrl(), - ] - ); + if ($this->shouldRedirectToCart()) { + $message = __( + 'You added %1 to your shopping cart.', + $product->getName() + ); + $this->messageManager->addSuccessMessage($message); + } else { + $this->messageManager->addComplexSuccessMessage( + 'addCartSuccessMessage', + [ + 'product_name' => $product->getName(), + 'cart_url' => $this->getCartUrl(), + ] + ); + } + if ($this->cart->getQuote()->getHasError()) { + $errors = $this->cart->getQuote()->getErrors(); + foreach ($errors as $error) { + $this->messageManager->addErrorMessage($error->getText()); } } return $this->goBack(null, $product); @@ -161,7 +163,6 @@ public function execute() } $url = $this->_checkoutSession->getRedirectUrl(true); - if (!$url) { $url = $this->_redirect->getRedirectUrl($this->getCartUrl()); } @@ -175,6 +176,8 @@ public function execute() $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); return $this->goBack(); } + + return $this->getResponse(); } /** @@ -182,7 +185,7 @@ public function execute() * * @param string $backUrl * @param \Magento\Catalog\Model\Product $product - * @return $this|\Magento\Framework\Controller\Result\Redirect + * @return ResponseInterface|ResultInterface */ protected function goBack($backUrl = null, $product = null) { @@ -205,6 +208,8 @@ protected function goBack($backUrl = null, $product = null) $this->getResponse()->representJson( $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($result) ); + + return $this->getResponse(); } /** 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/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index c33b784fcd20c..192f20653f8c3 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -105,7 +105,7 @@ <item name="trigger" xsi:type="string">opc-new-shipping-address</item> <item name="buttons" xsi:type="array"> <item name="save" xsi:type="array"> - <item name="text" xsi:type="string" translate="true">Ship here</item> + <item name="text" xsi:type="string" translate="true">Ship Here</item> <item name="class" xsi:type="string">action primary action-save-address</item> </item> <item name="cancel" xsi:type="array"> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js index c570bda51a80e..9adfb549a5b1c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js @@ -192,10 +192,6 @@ define([ * @returns {Boolean} - initial visibility state. */ resolveInitialPasswordVisibility: function () { - if (checkoutData.getInputFieldEmailValue() !== '' && checkoutData.getCheckedEmailValue() === '') { - return true; - } - if (checkoutData.getInputFieldEmailValue() !== '') { return checkoutData.getInputFieldEmailValue() === checkoutData.getCheckedEmailValue(); } diff --git a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html index 8e71d5845cec8..b7dfaffa558f3 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html @@ -21,6 +21,7 @@ data-bind=" textInput: email, hasFocus: emailFocused, + afterRender: emailHasChanged, mageInit: {'mage/trim-input':{}}" name="username" data-validate="{required:true, 'validate-email':true}" 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/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml index c46410dce919e..3687bb4fe5743 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml @@ -14,6 +14,7 @@ <stories value="Delete a CMS Page via the Admin"/> <title value="Admin should be able to delete CMS Pages"/> <description value="Admin should be able to delete CMS Pages"/> + <severity value="CRITICAL"/> <group value="Cms"/> <group value="WYSIWYGDisabled"/> </annotations> 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 e56225cbe2548..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 */ @@ -72,7 +75,7 @@ public function __construct( */ public function afterSave(ResourceStore $object, ResourceStore $result, AbstractModel $store): void { - if ($store->isObjectNew() || $store->dataHasChangedFor('group_id')) { + if ($store->isObjectNew()) { $this->urlPersist->replace( $this->generateCmsPagesUrls((int)$store->getId()) ); @@ -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/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php deleted file mode 100644 index 92b7ab0d88ea8..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; - -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Catalog\Model\Product; - -/** - * Extender of product identities for child of configurable products - */ -class ProductIdentitiesExtender -{ - /** - * @var Configurable - */ - private $configurableType; - - /** - * @param Configurable $configurableType - */ - public function __construct(Configurable $configurableType) - { - $this->configurableType = $configurableType; - } - - /** - * Add child identities to product identities - * - * @param Product $subject - * @param array $identities - * @return array - */ - public function afterGetIdentities(Product $subject, array $identities): array - { - foreach ($this->configurableType->getChildrenIds($subject->getId()) as $childIds) { - foreach ($childIds as $childId) { - $identities[] = Product::CACHE_TAG . '_' . $childId; - } - } - - return array_unique($identities); - } -} diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index b7bbf7aa1871c..9080314126ee1 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -134,8 +134,7 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE, iterator_to_array($entityIds) ); - $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false); - $this->tableMaintainer->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []); $this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds)); $this->applyConfigurableOption($temporaryPriceTable, $dimensions, iterator_to_array($entityIds)); @@ -224,8 +223,7 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar if ($entityIds !== null) { $select->where('le.entity_id IN (?)', $entityIds); } - $query = $select->insertFromSelect($temporaryOptionsTableName); - $this->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryOptionsTableName, []); } /** @@ -269,7 +267,7 @@ private function getMainTable($dimensions) if ($this->fullReindexAction) { return $this->tableMaintainer->getMainReplicaTable($dimensions); } - return $this->tableMaintainer->getMainTable($dimensions); + return $this->tableMaintainer->getMainTableByDimensions($dimensions); } /** 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/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml index bd409d0e4bfde..186799bf4626b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml @@ -65,10 +65,11 @@ <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" time="60" stepKey="waitForSuccessMessage"/> <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpdateSuccessMsg"/> - <!-- Run cron twice --> - <magentoCLI command="cron:run" arguments="--group=consumers" stepKey="runCron1"/> - <magentoCLI command="cron:run" arguments="--group=consumers" stepKey="runCron2"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Apply changes --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminProductAttributeUpdateMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminProductAttributeUpdateMessageConsumerData.messageLimit}}"/> + </actionGroup> <!-- Check storefront for description --> <amOnPage url="{{StorefrontProductPage.url($$createProduct1.custom_attributes[url_key]$$)}}" stepKey="goToFirstProductPageOnStorefront"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php deleted file mode 100644 index f96da3a7967bf..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin\Frontend; - -use Magento\Catalog\Model\Product; -use Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use PHPUnit\Framework\TestCase; - -class ProductIdentitiesExtenderTest extends TestCase -{ - /** - * @var \PHPUnit\Framework\MockObject\MockObject|Configurable - */ - private $configurableTypeMock; - - /** - * @var ProductIdentitiesExtender - */ - private $plugin; - - /** @var MockObject|Product */ - private $product; - - protected function setUp(): void - { - $this->product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->setMethods(['getId']) - ->getMock(); - - $this->configurableTypeMock = $this->getMockBuilder(Configurable::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock); - } - - public function testAfterGetIdentities() - { - $identities = [ - 'SomeCacheId', - 'AnotherCacheId', - ]; - $productId = 12345; - $childIdentities = [ - 0 => [1, 2, 5, 100500] - ]; - $expectedIdentities = [ - 'SomeCacheId', - 'AnotherCacheId', - Product::CACHE_TAG . '_' . 1, - Product::CACHE_TAG . '_' . 2, - Product::CACHE_TAG . '_' . 5, - Product::CACHE_TAG . '_' . 100500, - ]; - - $this->product->expects($this->once()) - ->method('getId') - ->willReturn($productId); - - $this->configurableTypeMock->expects($this->once()) - ->method('getChildrenIds') - ->with($productId) - ->willReturn($childIdentities); - - $productIdentities = $this->plugin->afterGetIdentities($this->product, $identities); - $this->assertEquals($expectedIdentities, $productIdentities); - } -} 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/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index b2d50f54f5334..f60234453dc60 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -10,9 +10,6 @@ <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> </type> - <type name="Magento\Catalog\Model\Product"> - <plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" /> - </type> <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable"> <plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> </type> 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/Data/ExtensionAttributeSimple.xml b/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimpleData.xml similarity index 100% rename from app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml rename to app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimpleData.xml 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 5f3ca2fdb7453..a349d07a5e222 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/email/change_email.html b/app/code/Magento/Customer/view/frontend/email/change_email.html index bd961ad99ec40..5341a2dc67ad5 100644 --- a/app/code/Magento/Customer/view/frontend/email/change_email.html +++ b/app/code/Magento/Customer/view/frontend/email/change_email.html @@ -18,8 +18,5 @@ {{trans "We have received a request to change the following information associated with your account at %store_name: email." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> -<br> - -<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html index 4f5c85b2381f3..ed2af7ada669e 100644 --- a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html +++ b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html @@ -18,8 +18,5 @@ {{trans "We have received a request to change the following information associated with your account at %store_name: email, password." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> -<br> - -<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset.html b/app/code/Magento/Customer/view/frontend/email/password_reset.html index cab05a89227b6..a6c54842a1573 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_reset.html +++ b/app/code/Magento/Customer/view/frontend/email/password_reset.html @@ -19,8 +19,5 @@ {{trans "We have received a request to change the following information associated with your account at %store_name: password." store_name=$store.frontend_name}} {{trans 'If you have not authorized this action, please contact us immediately at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. </p> -<br> - -<p>{{trans "Thanks,<br>%store_name" store_name=$store.frontend_name |raw}}</p> {{template config_path="design/email/footer_template"}} 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/Indexer/Price.php b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php index 90b458ff6348e..e50213f139ba1 100644 --- a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php @@ -100,10 +100,7 @@ public function __construct( } /** - * {@inheritdoc} - * @param array $dimensions - * @param \Traversable $entityIds - * @throws \Exception + * @inheritDoc */ public function executeByDimensions(array $dimensions, \Traversable $entityIds) { @@ -205,8 +202,7 @@ private function fillTemporaryTable(string $temporaryDownloadableTableName, arra 'max_price' => new \Zend_Db_Expr('SUM(' . $ifPrice . ')'), ] ); - $query = $select->insertFromSelect($temporaryDownloadableTableName); - $this->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryDownloadableTableName, []); } /** @@ -259,14 +255,13 @@ private function fillFinalPrice( IndexTableStructure $temporaryPriceTable ) { $select = $this->baseFinalPrice->getQuery($dimensions, Type::TYPE_DOWNLOADABLE, iterator_to_array($entityIds)); - $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false); - $this->tableMaintainer->getConnection()->query($query); + $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []); } /** * Get connection * - * return \Magento\Framework\DB\Adapter\AdapterInterface + * @return \Magento\Framework\DB\Adapter\AdapterInterface * @throws \DomainException */ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface 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/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml similarity index 100% rename from app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml rename to app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml 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/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index abdb064506641..245e4d494afe1 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -190,7 +190,7 @@ private function convertToProductData(int $productId, array $indexData, int $sto $attributeValues = [$productId => $attributeValues]; } $attributeValues = $this->prepareAttributeValues($productId, $attribute, $attributeValues, $storeId); - $productAttributes += $this->convertAttribute($attribute, $attributeValues); + $productAttributes += $this->convertAttribute($attribute, $attributeValues, $storeId); } return $productAttributes; @@ -201,18 +201,19 @@ private function convertToProductData(int $productId, array $indexData, int $sto * * @param Attribute $attribute * @param array $attributeValues + * @param int $storeId * @return array */ - private function convertAttribute(Attribute $attribute, array $attributeValues): array + private function convertAttribute(Attribute $attribute, array $attributeValues, int $storeId): array { $productAttributes = []; $retrievedValue = $this->retrieveFieldValue($attributeValues); - if ($retrievedValue) { + if ($retrievedValue !== null) { $productAttributes[$attribute->getAttributeCode()] = $retrievedValue; if ($attribute->getIsSearchable()) { - $attributeLabels = $this->getValuesLabels($attribute, $attributeValues); + $attributeLabels = $this->getValuesLabels($attribute, $attributeValues, $storeId); $retrievedLabel = $this->retrieveFieldValue($attributeLabels); if ($retrievedLabel) { $productAttributes[$attribute->getAttributeCode() . '_value'] = $retrievedLabel; @@ -299,20 +300,21 @@ private function isAttributeDate(Attribute $attribute): bool * * @param Attribute $attribute * @param array $attributeValues + * @param int $storeId * @return array */ - private function getValuesLabels(Attribute $attribute, array $attributeValues): array + private function getValuesLabels(Attribute $attribute, array $attributeValues, int $storeId): array { $attributeLabels = []; - $options = $this->getAttributeOptions($attribute); + $options = $this->getAttributeOptions($attribute, $storeId); if (empty($options)) { return $attributeLabels; } - foreach ($attributeValues as $attributeValue) { - if (isset($options[$attributeValue])) { - $attributeLabels[] = $options[$attributeValue]->getLabel(); + foreach ($options as $option) { + if (\in_array($option['value'], $attributeValues)) { + $attributeLabels[] = $option['label']; } } @@ -323,20 +325,23 @@ private function getValuesLabels(Attribute $attribute, array $attributeValues): * Retrieve options for attribute * * @param Attribute $attribute + * @param int $storeId * @return array */ - private function getAttributeOptions(Attribute $attribute): array + private function getAttributeOptions(Attribute $attribute, int $storeId): array { - if (!isset($this->attributeOptionsCache[$attribute->getId()])) { - $options = $attribute->getOptions() ?? []; - $optionsByValue = []; - foreach ($options as $option) { - $optionsByValue[$option->getValue()] = $option; - } - $this->attributeOptionsCache[$attribute->getId()] = $optionsByValue; + if (!isset($this->attributeOptionsCache[$storeId][$attribute->getId()])) { + $attributeStoreId = $attribute->getStoreId(); + /** + * Load array format of options. + * $attribute->getOptions() loads options into data objects which can be costly. + */ + $options = $attribute->usesSource() ? $attribute->setStoreId($storeId)->getSource()->getAllOptions() : []; + $this->attributeOptionsCache[$storeId][$attribute->getId()] = $options; + $attribute->setStoreId($attributeStoreId); } - return $this->attributeOptionsCache[$attribute->getId()]; + return $this->attributeOptionsCache[$storeId][$attribute->getId()]; } /** @@ -349,7 +354,7 @@ private function getAttributeOptions(Attribute $attribute): array */ private function retrieveFieldValue(array $values) { - $values = \array_filter(\array_unique($values)); + $values = \array_unique($values); return count($values) === 1 ? \array_shift($values) : \array_values($values); } 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/SearchAdapter/Filter/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php index 76a2f00f44fe2..ce79f433460d9 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php @@ -68,7 +68,7 @@ public function buildFilter(RequestFilterInterface $filter) $fieldName .= '.' . $suffix; } - if ($filter->getValue()) { + if ($filter->getValue() !== false) { $operator = is_array($filter->getValue()) ? 'terms' : 'term'; $filterQuery []= [ $operator => [ 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/BatchDataMapper/ProductDataMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php index 85be064d40b3a..2c87549da6075 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface; use Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper; use Magento\Elasticsearch\Model\Adapter\Document\Builder; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; @@ -199,11 +200,17 @@ public function testGetMapWithOptions() */ private function getAttribute(array $attributeData): MockObject { - $attributeMock = $this->createMock(Attribute::class); - $attributeMock->method('getAttributeCode')->willReturn($attributeData['code']); - $attributeMock->method('getBackendType')->willReturn($attributeData['backendType']); - $attributeMock->method('getFrontendInput')->willReturn($attributeData['frontendInput']); - $attributeMock->method('getIsSearchable')->willReturn($attributeData['is_searchable']); + $attributeMock = $this->createPartialMock( + Attribute::class, + [ + 'getSource', + 'getOptions', + ] + ); + + $sourceMock = $this->createMock(SourceInterface::class); + $attributeMock->method('getSource')->willReturn($sourceMock); + $sourceMock->method('getAllOptions')->willReturn($attributeData['options'] ?? []); $options = []; foreach ($attributeData['options'] as $option) { $optionMock = $this->getMockForAbstractClass(AttributeOptionInterface::class); @@ -212,6 +219,8 @@ private function getAttribute(array $attributeData): MockObject $options[] = $optionMock; } $attributeMock->method('getOptions')->willReturn($options); + unset($attributeData['options']); + $attributeMock->setData($attributeData); return $attributeMock; } @@ -226,9 +235,9 @@ public static function mapProvider(): array 'text' => [ 10, [ - 'code' => 'description', - 'backendType' => 'text', - 'frontendInput' => 'text', + 'attribute_code' => 'description', + 'backend_type' => 'text', + 'frontend_input' => 'text', 'is_searchable' => false, 'options' => [], ], @@ -238,9 +247,9 @@ public static function mapProvider(): array 'datetime' => [ 10, [ - 'code' => 'created_at', - 'backendType' => 'datetime', - 'frontendInput' => 'date', + 'attribute_code' => 'created_at', + 'backend_type' => 'datetime', + 'frontend_input' => 'date', 'is_searchable' => false, 'options' => [], ], @@ -251,9 +260,9 @@ public static function mapProvider(): array 'array single value' => [ 10, [ - 'code' => 'attribute_array', - 'backendType' => 'text', - 'frontendInput' => 'text', + 'attribute_code' => 'attribute_array', + 'backend_type' => 'text', + 'frontend_input' => 'text', 'is_searchable' => false, 'options' => [], ], @@ -263,9 +272,9 @@ public static function mapProvider(): array 'array multiple value' => [ 10, [ - 'code' => 'attribute_array', - 'backendType' => 'text', - 'frontendInput' => 'text', + 'attribute_code' => 'attribute_array', + 'backend_type' => 'text', + 'frontend_input' => 'text', 'is_searchable' => false, 'options' => [], ], @@ -275,9 +284,9 @@ public static function mapProvider(): array 'array multiple decimal value' => [ 10, [ - 'code' => 'decimal_array', - 'backendType' => 'decimal', - 'frontendInput' => 'text', + 'attribute_code' => 'decimal_array', + 'backend_type' => 'decimal', + 'frontend_input' => 'text', 'is_searchable' => false, 'options' => [], ], @@ -287,9 +296,9 @@ public static function mapProvider(): array 'array excluded from merge' => [ 10, [ - 'code' => 'status', - 'backendType' => 'int', - 'frontendInput' => 'select', + 'attribute_code' => 'status', + 'backend_type' => 'int', + 'frontend_input' => 'select', 'is_searchable' => false, 'options' => [ ['value' => '1', 'label' => 'Enabled'], @@ -302,9 +311,9 @@ public static function mapProvider(): array 'select without options' => [ 10, [ - 'code' => 'color', - 'backendType' => 'text', - 'frontendInput' => 'select', + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', 'is_searchable' => false, 'options' => [], ], @@ -314,9 +323,9 @@ public static function mapProvider(): array 'unsearchable select with options' => [ 10, [ - 'code' => 'color', - 'backendType' => 'text', - 'frontendInput' => 'select', + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', 'is_searchable' => false, 'options' => [ ['value' => '44', 'label' => 'red'], @@ -329,9 +338,9 @@ public static function mapProvider(): array 'searchable select with options' => [ 10, [ - 'code' => 'color', - 'backendType' => 'text', - 'frontendInput' => 'select', + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', 'is_searchable' => true, 'options' => [ ['value' => '44', 'label' => 'red'], @@ -344,9 +353,9 @@ public static function mapProvider(): array 'composite select with options' => [ 10, [ - 'code' => 'color', - 'backendType' => 'text', - 'frontendInput' => 'select', + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', 'is_searchable' => true, 'options' => [ ['value' => '44', 'label' => 'red'], @@ -359,9 +368,9 @@ public static function mapProvider(): array 'multiselect without options' => [ 10, [ - 'code' => 'multicolor', - 'backendType' => 'text', - 'frontendInput' => 'multiselect', + 'attribute_code' => 'multicolor', + 'backend_type' => 'text', + 'frontend_input' => 'multiselect', 'is_searchable' => false, 'options' => [], ], @@ -371,9 +380,9 @@ public static function mapProvider(): array 'unsearchable multiselect with options' => [ 10, [ - 'code' => 'multicolor', - 'backendType' => 'text', - 'frontendInput' => 'multiselect', + 'attribute_code' => 'multicolor', + 'backend_type' => 'text', + 'frontend_input' => 'multiselect', 'is_searchable' => false, 'options' => [ ['value' => '44', 'label' => 'red'], @@ -386,9 +395,9 @@ public static function mapProvider(): array 'searchable multiselect with options' => [ 10, [ - 'code' => 'multicolor', - 'backendType' => 'text', - 'frontendInput' => 'multiselect', + 'attribute_code' => 'multicolor', + 'backend_type' => 'text', + 'frontend_input' => 'multiselect', 'is_searchable' => true, 'options' => [ ['value' => '44', 'label' => 'red'], @@ -401,9 +410,9 @@ public static function mapProvider(): array 'composite multiselect with options' => [ 10, [ - 'code' => 'multicolor', - 'backendType' => 'text', - 'frontendInput' => 'multiselect', + 'attribute_code' => 'multicolor', + 'backend_type' => 'text', + 'frontend_input' => 'multiselect', 'is_searchable' => true, 'options' => [ ['value' => '44', 'label' => 'red'], @@ -417,9 +426,9 @@ public static function mapProvider(): array 'excluded attribute' => [ 10, [ - 'code' => 'price', - 'backendType' => 'int', - 'frontendInput' => 'int', + 'attribute_code' => 'price', + 'backend_type' => 'int', + 'frontend_input' => 'int', 'is_searchable' => false, 'options' => [] ], 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 new file mode 100644 index 0000000000000..2ce51c8bbf19d --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessageGraphQl\Model\Resolver\Cart; + +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\CartRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; + +/** + * Class provides ability to get GiftMessage for cart + */ +class GiftMessage implements ResolverInterface +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @param CartRepositoryInterface $cartRepository + * @param GiftMessageHelper $giftMessageHelper + */ + public function __construct( + CartRepositoryInterface $cartRepository, + GiftMessageHelper $giftMessageHelper + ) { + $this->cartRepository = $cartRepository; + $this->giftMessageHelper = $giftMessageHelper; + } + + /** + * Return information about Gift message of 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')); + } + + $cart = $value['model']; + + if (!$this->giftMessageHelper->isMessagesAllowed('order', $cart)) { + return null; + } + + try { + $giftCartMessage = $this->cartRepository->get($cart->getId()); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Can\'t load cart.')); + } + + if (!isset($giftCartMessage)) { + return null; + } + + return [ + 'to' => $giftCartMessage->getRecipient() ?? '', + 'from' => $giftCartMessage->getSender() ?? '', + 'message'=> $giftCartMessage->getMessage() ?? '' + ]; + } +} 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/Model/Resolver/Order/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php new file mode 100644 index 0000000000000..aae0e3709d87f --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessageGraphQl\Model\Resolver\Order; + +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\OrderRepositoryInterface; + +/** + * Class for getting GiftMessage from CustomerOrder + */ +class GiftMessage implements ResolverInterface +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @param OrderRepositoryInterface $orderRepository + */ + public function __construct( + OrderRepositoryInterface $orderRepository + ) { + $this->orderRepository = $orderRepository; + } + + /** + * Return information about gift message for order + * + * @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['id'])) { + throw new GraphQlInputException(__('"id" value should be specified')); + } + + try { + $orderGiftMessage = $this->orderRepository->get($value['id']); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Can\'t load gift message for order')); + } + + if (!isset($orderGiftMessage)) { + return null; + } + + return [ + 'to' => $orderGiftMessage->getRecipient() ?? '', + 'from' => $orderGiftMessage->getSender() ?? '', + 'message'=> $orderGiftMessage->getMessage() ?? '' + ]; + } +} diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md new file mode 100644 index 0000000000000..d73a058e0db9c --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/README.md @@ -0,0 +1,3 @@ +# GiftMessageGraphQl + +**GiftMessageGraphQl** provides information about gift messages for carts, cart items, orders and order items. diff --git a/app/code/Magento/GiftMessageGraphQl/composer.json b/app/code/Magento/GiftMessageGraphQl/composer.json new file mode 100644 index 0000000000000..48088f2a48a32 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-gift-message-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-gift-message": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\GiftMessageGraphQl\\": "" + } + } +} 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/module.xml b/app/code/Magento/GiftMessageGraphQl/etc/module.xml new file mode 100644 index 0000000000000..5eaaae0b0b988 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/etc/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_GiftMessageGraphQl"> + <sequence> + <module name="Magento_GiftMessage"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..ad18054abca13 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls @@ -0,0 +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 SimpleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +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") +} + +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/GiftMessageGraphQl/registration.php b/app/code/Magento/GiftMessageGraphQl/registration.php new file mode 100644 index 0000000000000..bb260c23b0177 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_GiftMessageGraphQl', + __DIR__ +); 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 2bcd44e9ae410..23d49124d1a02 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -15,10 +15,6 @@ <item name="type" xsi:type="object">Magento\Webapi\Model\Authorization\TokenUserContext</item> <item name="sortOrder" xsi:type="string">10</item> </item> - <item name="oauthUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\Webapi\Model\Authorization\OauthUserContext</item> - <item name="sortOrder" xsi:type="string">40</item> - </item> <item name="guestUserContext" xsi:type="array"> <item name="type" xsi:type="object">Magento\Webapi\Model\Authorization\GuestUserContext</item> <item name="sortOrder" xsi:type="string">100</item> @@ -34,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/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php index e1599dc772c2c..c5f0316feb126 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php @@ -20,6 +20,7 @@ /** * Calculate minimal and maximal prices for Grouped products + * * Use calculated price for relation products * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -85,10 +86,7 @@ public function __construct( } /** - * {@inheritdoc} - * @param array $dimensions - * @param \Traversable $entityIds - * @throws \Exception + * @inheritDoc */ public function executeByDimensions(array $dimensions, \Traversable $entityIds) { @@ -105,9 +103,8 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) 'maxPriceField' => 'max_price', 'tierPriceField' => 'tier_price', ]); - $query = $this->prepareGroupedProductPriceDataSelect($dimensions, iterator_to_array($entityIds)) - ->insertFromSelect($temporaryPriceTable->getTableName()); - $this->getConnection()->query($query); + $select = $this->prepareGroupedProductPriceDataSelect($dimensions, iterator_to_array($entityIds)); + $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []); } /** @@ -186,13 +183,13 @@ private function getMainTable($dimensions) if ($this->fullReindexAction) { return $this->tableMaintainer->getMainReplicaTable($dimensions); } - return $this->tableMaintainer->getMainTable($dimensions); + return $this->tableMaintainer->getMainTableByDimensions($dimensions); } /** * Get connection * - * return \Magento\Framework\DB\Adapter\AdapterInterface + * @return \Magento\Framework\DB\Adapter\AdapterInterface * @throws \DomainException */ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml index aaa9cf5b2f925..e966ccf82648f 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml @@ -21,7 +21,6 @@ <skip> <issueId value="MC-34217"/> </skip> - </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> 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/Indexer/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php index 7032c9258cae6..d86b15134283b 100644 --- a/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php @@ -11,6 +11,7 @@ /** * Abstract resource model. Can be used as base for indexer resources * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -120,8 +121,7 @@ public function insertFromTable($sourceTable, $destTable, $readToIndex = true) } /** - * Insert data from select statement of read adapter to - * destination table related with index adapter + * Insert data from select statement of read adapter to destination table related with index adapter * * @param Select $select * @param string $destTable @@ -141,6 +141,9 @@ public function insertFromSelect($select, $destTable, array $columns, $readToInd if ($from === $to) { $query = $select->insertFromSelect($destTable, $columns); + if ($to->getTransactionLevel() === 0) { + $to->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED;'); + } $to->query($query); } else { $stmt = $from->query($select); diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerReindexActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerReindexActionGroup.xml new file mode 100644 index 0000000000000..5556707a4d494 --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerReindexActionGroup.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="CliIndexerReindexActionGroup"> + <annotations> + <description>Run reindex by CLI with specified indexers (space separated).</description> + </annotations> + <arguments> + <argument name="indices" type="string"/> + </arguments> + + <magentoCLI command="indexer:reindex" arguments="{{indices}}" stepKey="reindexSpecifiedIndexers"/> + </actionGroup> +</actionGroups> 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/MessageQueue/Test/Mftf/ActionGroup/CliConsumerStartActionGroup.xml b/app/code/Magento/MessageQueue/Test/Mftf/ActionGroup/CliConsumerStartActionGroup.xml new file mode 100644 index 0000000000000..204691f1ad5e5 --- /dev/null +++ b/app/code/Magento/MessageQueue/Test/Mftf/ActionGroup/CliConsumerStartActionGroup.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="CliConsumerStartActionGroup"> + <annotations> + <description>Starts message queue for specific consumer by CLI.</description> + </annotations> + <arguments> + <argument name="consumerName" type="string"/> + <argument name="maxMessages" type="string"/> + </arguments> + + <magentoCLI command="queue:consumers:start {{consumerName}} --max-messages={{maxMessages}}" stepKey="startMessageQueue"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MessageQueue/Test/Mftf/Data/QueueConsumerData.xml b/app/code/Magento/MessageQueue/Test/Mftf/Data/QueueConsumerData.xml new file mode 100644 index 0000000000000..ef989808eea60 --- /dev/null +++ b/app/code/Magento/MessageQueue/Test/Mftf/Data/QueueConsumerData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminExportMessageConsumerData"> + <data key="consumerName">exportProcessor</data> + <data key="messageLimit">100</data> + </entity> + <entity name="AdminProductAttributeUpdateMessageConsumerData"> + <data key="consumerName">product_action_attribute.update</data> + <data key="messageLimit">100</data> + </entity> + <entity name="AdminCodeGeneratorMessageConsumerData"> + <data key="consumerName">codegeneratorProcessor</data> + <data key="messageLimit">100</data> + </entity> +</entities> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml index 02187658a8781..815d406c68bfa 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml @@ -19,12 +19,6 @@ <group value="Multishipment"/> <group value="SalesRule"/> </annotations> - <before> - <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> - </before> - <after> - <magentoCLI command="config:set multishipping/options/checkout_multiple 0" stepKey="disableShippingToMultipleAddresses"/> - </after> <actionGroup ref="AdminCreateCartPriceRuleActionsWithSubtotalActionGroup" before="goToProduct1" stepKey="createSubtotalCartPriceRuleActionsSection"> <argument name="ruleName" value="CartPriceRuleConditionForSubtotalForMultiShipping"/> </actionGroup> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml index a49a37e475409..8205ab962b9fe 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml @@ -22,8 +22,6 @@ <before> <!-- Login as Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <!-- Set configurations --> - <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> <!-- Create simple products --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="firstProduct"> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml index 80407a219a841..2e5c0acc32053 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml @@ -27,7 +27,6 @@ <createData entity="SimpleProduct2" stepKey="createProduct2"/> <createData entity="Simple_US_Customer_Two_Addresses" stepKey="createCustomer"/> <!-- Set configurations --> - <magentoCLI command="config:set {{EnableMultiShippingCheckoutMultiple.path}} {{EnableMultiShippingCheckoutMultiple.value}}" stepKey="allowShippingToMultipleAddresses"/> <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> @@ -43,7 +42,6 @@ <!-- Need logout before customer delete. Fatal error appears otherwise --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <magentoCLI command="config:set {{DisableMultiShippingCheckoutMultiple.path}} {{DisableMultiShippingCheckoutMultiple.value}}" stepKey="withdrawShippingToMultipleAddresses"/> <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml index caf0ce3a51bae..7bb26525b173f 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml @@ -22,8 +22,6 @@ <before> <!-- Login as Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <!-- Set configurations --> - <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> <!-- Create two simple products --> <createData entity="ApiCategory" stepKey="createCategory"/> <createData entity="_defaultProduct" stepKey="createFirstProduct"> diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php index 6391219e23c7e..2519dd3a6fea8 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php @@ -116,6 +116,7 @@ public function setMessagesScope($scope) * @param string $email * @param int $websiteId * @return array + * @throws LocalizedException */ public function loadBySubscriberEmail(string $email, int $websiteId): array { diff --git a/app/code/Magento/Newsletter/i18n/en_US.csv b/app/code/Magento/Newsletter/i18n/en_US.csv index f390f6792635d..f8706967117fe 100644 --- a/app/code/Magento/Newsletter/i18n/en_US.csv +++ b/app/code/Magento/Newsletter/i18n/en_US.csv @@ -153,3 +153,6 @@ Store,Store "Newsletter Subscriptions","Newsletter Subscriptions" "We have updated your subscription.","We have updated your subscription." "Are you sure you want to delete the selected subscriber(s)?","Are you sure you want to delete the selected subscriber(s)?" +"Cannot create a newsletter subscription.","Cannot create a newsletter subscription." +"Enter a valid email address.","Enter a valid email address." +"Guests can not subscribe to the newsletter. You must create an account to subscribe.","Guests can not subscribe to the newsletter. You must create an account to subscribe." diff --git a/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html b/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html index d56163c10fdf3..996dff0c973e9 100644 --- a/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html +++ b/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html @@ -13,6 +13,6 @@ {{template config_path="design/email/header_template"}} -{{trans "You have been successfully subscribed to our newsletter."}} +<p>{{trans "You have been successfully subscribed to our newsletter."}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html b/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html index d39b5d8a8b8e9..1f222f85abac7 100644 --- a/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html +++ b/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html @@ -13,6 +13,6 @@ {{template config_path="design/email/header_template"}} -{{trans "You have been unsubscribed from the newsletter."}} +<p>{{trans "You have been unsubscribed from the newsletter."}}</p> {{template config_path="design/email/footer_template"}} 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/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php b/app/code/Magento/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php new file mode 100644 index 0000000000000..a4b3bc43d0a8b --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\NewsletterGraphQl\Model\Resolver; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\EnumLookup; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Newsletter\Model\SubscriptionManagerInterface; +use Magento\NewsletterGraphQl\Model\SubscribeEmailToNewsletter\Validation; +use Psr\Log\LoggerInterface; + +/** + * Resolver class for the `subscribeEmailToNewsletter` mutation. Adds an email into a newsletter subscription. + */ +class SubscribeEmailToNewsletter implements ResolverInterface +{ + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var EnumLookup + */ + private $enumLookup; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var SubscriptionManagerInterface + */ + private $subscriptionManager; + + /** + * @var Validation + */ + private $validator; + + /** + * SubscribeEmailToNewsletter constructor. + * + * @param CustomerRepositoryInterface $customerRepository + * @param EnumLookup $enumLookup + * @param LoggerInterface $logger + * @param SubscriptionManagerInterface $subscriptionManager + * @param Validation $validator + */ + public function __construct( + CustomerRepositoryInterface $customerRepository, + EnumLookup $enumLookup, + LoggerInterface $logger, + SubscriptionManagerInterface $subscriptionManager, + Validation $validator + ) { + $this->customerRepository = $customerRepository; + $this->enumLookup = $enumLookup; + $this->logger = $logger; + $this->subscriptionManager = $subscriptionManager; + $this->validator = $validator; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $email = trim($args['email']); + + if (empty($email)) { + throw new GraphQlInputException( + __('You must specify an email address to subscribe to a newsletter.') + ); + } + + $currentUserId = (int)$context->getUserId(); + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + $websiteId = (int)$context->getExtensionAttributes()->getStore()->getWebsiteId(); + + $this->validator->execute($email, $currentUserId, $websiteId); + + try { + $subscriber = $this->isCustomerSubscription($email, $currentUserId) + ? $this->subscriptionManager->subscribeCustomer($currentUserId, $storeId) + : $this->subscriptionManager->subscribe($email, $storeId); + + $status = $this->enumLookup->getEnumValueFromField( + 'SubscriptionStatusesEnum', + (string)$subscriber->getSubscriberStatus() + ); + } catch (LocalizedException $e) { + $this->logger->error($e->getMessage()); + + throw new GraphQlInputException( + __('Cannot create a newsletter subscription.') + ); + } + + return [ + 'status' => $status + ]; + } + + /** + * Returns true if a provided email equals to a current customer one + * + * @param string $email + * @param int $currentUserId + * @return bool + * @throws LocalizedException + * @throws NoSuchEntityException + */ + private function isCustomerSubscription(string $email, int $currentUserId): bool + { + if ($currentUserId > 0) { + $customer = $this->customerRepository->getById($currentUserId); + + if ($customer->getEmail() == $email) { + return true; + } + } + + return false; + } +} diff --git a/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php b/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php new file mode 100644 index 0000000000000..8b8cac0c58cf2 --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\NewsletterGraphQl\Model\SubscribeEmailToNewsletter; + +use Magento\Customer\Api\AccountManagementInterface as CustomerAccountManagement; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Validator\EmailAddress as EmailValidator; +use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResourceModel; +use Magento\Newsletter\Model\Subscriber; +use Magento\Store\Model\ScopeInterface; +use Psr\Log\LoggerInterface; + +/** + * Validation class for the "subscribeEmailToNewsletter" mutation + */ +class Validation +{ + /** + * @var CustomerAccountManagement + */ + private $customerAccountManagement; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var EmailValidator + */ + private $emailValidator; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var SubscriberResourceModel + */ + private $subscriberResource; + + /** + * Validation constructor. + * + * @param CustomerAccountManagement $customerAccountManagement + * @param CustomerRepositoryInterface $customerRepository + * @param EmailValidator $emailValidator + * @param LoggerInterface $logger + * @param ScopeConfigInterface $scopeConfig + * @param SubscriberResourceModel $subscriberResource + */ + public function __construct( + CustomerAccountManagement $customerAccountManagement, + CustomerRepositoryInterface $customerRepository, + EmailValidator $emailValidator, + LoggerInterface $logger, + ScopeConfigInterface $scopeConfig, + SubscriberResourceModel $subscriberResource + ) { + $this->customerAccountManagement = $customerAccountManagement; + $this->customerRepository = $customerRepository; + $this->emailValidator = $emailValidator; + $this->logger = $logger; + $this->scopeConfig = $scopeConfig; + $this->subscriberResource = $subscriberResource; + } + + /** + * Validate the next cases: + * - email format + * - email address isn't being used by a different account + * - if a guest user can be subscribed to a newsletter + * - verify if email is already subscribed + * + * @param string $email + * @param int $currentUserId + * @param int $websiteId + * @throws GraphQlAlreadyExistsException + * @throws GraphQlInputException + */ + public function execute(string $email = '', int $currentUserId = 0, int $websiteId = 1): void + { + $this->validateEmailFormat($email); + + if ($currentUserId > 0) { + $this->validateEmailAvailable($email, $currentUserId, $websiteId); + } else { + $this->validateGuestSubscription(); + } + + $this->validateAlreadySubscribed($email, $websiteId); + } + + /** + * Validate the format of the email address + * + * @param string $email + * @throws GraphQlInputException + */ + private function validateEmailFormat(string $email): void + { + if (!$this->emailValidator->isValid($email)) { + throw new GraphQlInputException(__('Enter a valid email address.')); + } + } + + /** + * Validate that the email address isn't being used by a different account. + * + * @param string $email + * @param int $currentUserId + * @param int $websiteId + * @throws GraphQlInputException + */ + private function validateEmailAvailable(string $email, int $currentUserId, int $websiteId): void + { + try { + $customer = $this->customerRepository->getById($currentUserId); + $customerEmail = $customer->getEmail(); + } catch (LocalizedException $e) { + $customerEmail = ''; + } + + try { + $emailAvailable = $this->customerAccountManagement->isEmailAvailable($email, $websiteId); + } catch (LocalizedException $e) { + $emailAvailable = false; + } + + if (!$emailAvailable && $customerEmail != $email) { + $this->logger->error( + __('This email address is already assigned to another user.') + ); + + throw new GraphQlInputException( + __('Cannot create a newsletter subscription.') + ); + } + } + + /** + * Validate if a guest user can be subscribed to a newsletter. + * + * @throws GraphQlInputException + */ + private function validateGuestSubscription(): void + { + if (!$this->scopeConfig->getValue( + Subscriber::XML_PATH_ALLOW_GUEST_SUBSCRIBE_FLAG, + ScopeInterface::SCOPE_STORE + )) { + throw new GraphQlInputException( + __('Guests can not subscribe to the newsletter. You must create an account to subscribe.') + ); + } + } + + /** + * Verify if email is already subscribed + * + * @param string $email + * @param int $websiteId + * @throws GraphQlAlreadyExistsException + */ + private function validateAlreadySubscribed(string $email, int $websiteId): void + { + try { + $subscriberData = $this->subscriberResource->loadBySubscriberEmail($email, $websiteId); + } catch (LocalizedException $e) { + $subscriberData = []; + } + + if (isset($subscriberData['subscriber_status']) + && (int)$subscriberData['subscriber_status'] === Subscriber::STATUS_SUBSCRIBED) { + throw new GraphQlAlreadyExistsException( + __('This email address is already subscribed.') + ); + } + } +} diff --git a/app/code/Magento/NewsletterGraphQl/README.md b/app/code/Magento/NewsletterGraphQl/README.md new file mode 100644 index 0000000000000..c65d44fbcfeba --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/README.md @@ -0,0 +1 @@ +The Magento_NewsletterGraphQl module allows a shopper to subscribe to a newsletter using GraphQL. diff --git a/app/code/Magento/NewsletterGraphQl/composer.json b/app/code/Magento/NewsletterGraphQl/composer.json new file mode 100644 index 0000000000000..92352a8a9adfe --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-newsletter-graph-ql", + "description": "Provides GraphQl functionality for the newsletter subscriptions.", + "config": { + "sort-packages": true + }, + "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-customer": "*", + "magento/module-newsletter": "*", + "magento/module-store": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\NewsletterGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml b/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..302a562ec4700 --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml @@ -0,0 +1,21 @@ +<?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\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="SubscriptionStatusesEnum" xsi:type="array"> + <item name="subscribed" xsi:type="string">1</item> + <item name="not_active" xsi:type="string">2</item> + <item name="unsubscribed" xsi:type="string">3</item> + <item name="unconfirmed" xsi:type="string">4</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/NewsletterGraphQl/etc/module.xml b/app/code/Magento/NewsletterGraphQl/etc/module.xml new file mode 100644 index 0000000000000..8bda85d80c830 --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/etc/module.xml @@ -0,0 +1,14 @@ +<?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:Module/etc/module.xsd"> + <module name="Magento_NewsletterGraphQl"> + <sequence> + <module name="Magento_Newsletter"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls b/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..d96756e12caea --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls @@ -0,0 +1,17 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Mutation { + subscribeEmailToNewsletter(email: String!): SubscribeEmailToNewsletterOutput @doc(description:"Subscribes the specified email to a newsletter") @resolver(class: "Magento\\NewsletterGraphQl\\Model\\Resolver\\SubscribeEmailToNewsletter") +} + +type SubscribeEmailToNewsletterOutput { + status: SubscriptionStatusesEnum @doc(description: "Returns the status of the subscription request") +} + +enum SubscriptionStatusesEnum { + NOT_ACTIVE + SUBSCRIBED + UNSUBSCRIBED + UNCONFIRMED +} diff --git a/app/code/Magento/NewsletterGraphQl/registration.php b/app/code/Magento/NewsletterGraphQl/registration.php new file mode 100644 index 0000000000000..82d5512f4afb5 --- /dev/null +++ b/app/code/Magento/NewsletterGraphQl/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_NewsletterGraphQl', __DIR__); 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/Paypal/Model/Payflowpro.php b/app/code/Magento/Paypal/Model/Payflowpro.php index 778cd0c728de3..f8d17c5a5fdfe 100644 --- a/app/code/Magento/Paypal/Model/Payflowpro.php +++ b/app/code/Magento/Paypal/Model/Payflowpro.php @@ -8,6 +8,8 @@ use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\State\InvalidTransitionException; +use Magento\Payment\Gateway\Command\CommandException; use Magento\Payment\Helper\Formatter; use Magento\Payment\Model\InfoInterface; use Magento\Payment\Model\Method\ConfigInterface; @@ -85,6 +87,8 @@ class Payflowpro extends \Magento\Payment\Model\Method\Cc implements GatewayInte const RESPONSE_CODE_VOID_ERROR = 108; + private const RESPONSE_CODE_AUTHORIZATION_EXPIRED = 10601; + const PNREF = 'pnref'; /**#@-*/ @@ -376,7 +380,7 @@ public function getConfigPaymentAction() * @param float $amount * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws InvalidTransitionException */ public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount) { @@ -410,7 +414,7 @@ protected function _getCaptureAmount($amount) * @param float $amount * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws InvalidTransitionException */ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) { @@ -448,7 +452,7 @@ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) * @param InfoInterface|Payment|Object $payment * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws InvalidTransitionException */ public function void(\Magento\Payment\Model\InfoInterface $payment) { @@ -491,14 +495,23 @@ public function canVoid() * * @param InfoInterface|Object $payment * @return $this + * @throws CommandException */ public function cancel(\Magento\Payment\Model\InfoInterface $payment) { if (!$payment->getOrder()->getInvoiceCollection()->count()) { - return $this->void($payment); + try { + $this->void($payment); + } catch (CommandException $e) { + // Ignore error about expiration of authorization transaction. + if (strpos($e->getMessage(), (string)self::RESPONSE_CODE_AUTHORIZATION_EXPIRED) === false) { + throw $e; + } + } + } - return false; + return $this; } /** @@ -508,7 +521,7 @@ public function cancel(\Magento\Payment\Model\InfoInterface $payment) * @param float $amount * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws InvalidTransitionException */ public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount) { @@ -558,7 +571,7 @@ public function fetchTransactionInfo(InfoInterface $payment, $transactionId) * @return bool * phpcs:disable Magento2.Functions.StaticFunction */ - protected static function _isTransactionUnderReview($status) + protected function _isTransactionUnderReview($status) { if (in_array($status, [self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_DECLINED_BY_MERCHANT])) { return false; @@ -650,21 +663,22 @@ public function buildBasicRequest() * * @param DataObject $response * @return void - * @throws \Magento\Payment\Gateway\Command\CommandException - * @throws \Magento\Framework\Exception\State\InvalidTransitionException + * @throws CommandException + * @throws InvalidTransitionException */ public function processErrors(DataObject $response) { - if ($response->getResultCode() == self::RESPONSE_CODE_VOID_ERROR) { - throw new \Magento\Framework\Exception\State\InvalidTransitionException( + $resultCode = (int)$response->getResultCode(); + if ($resultCode === self::RESPONSE_CODE_VOID_ERROR) { + throw new InvalidTransitionException( __("The verification transaction can't be voided. ") ); - } elseif ($response->getResultCode() != self::RESPONSE_CODE_APPROVED && - $response->getResultCode() != self::RESPONSE_CODE_FRAUDSERVICE_FILTER - ) { - throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg())); - } elseif ($response->getOrigresult() == self::RESPONSE_CODE_DECLINED_BY_FILTER) { - throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg())); + } + if (!in_array($resultCode, [self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_FRAUDSERVICE_FILTER])) { + throw new CommandException(__($response->getRespmsg())); + } + if ((int)$response->getOrigresult() === self::RESPONSE_CODE_DECLINED_BY_FILTER) { + throw new CommandException(__($response->getRespmsg())); } } diff --git a/app/code/Magento/Persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php b/app/code/Magento/Persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php new file mode 100644 index 0000000000000..1c2b180c5dd1a --- /dev/null +++ b/app/code/Magento/Persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model\Checkout; + +use Magento\Checkout\Api\Data\PaymentDetailsInterface; +use Magento\Checkout\Model\GuestShippingInformationManagement; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Persistent\Helper\Data; +use Magento\Persistent\Helper\Session as PersistentSession; +use Magento\Persistent\Model\QuoteManager; + +/** + * Plugin to convert shopping cart from persistent cart to guest cart after shipping information saved + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class GuestShippingInformationManagementPlugin +{ + /** + * Persistence Session Helper + * + * @var PersistentSession + */ + private $persistenceSessionHelper; + + /** + * Persistence Data Helper + * + * @var Data + */ + private $persistenceDataHelper; + + /** + * Customer Session + * + * @var CustomerSession + */ + private $customerSession; + + /** + * Quote Manager + * + * @var QuoteManager + */ + private $quoteManager; + + /** + * Initialize dependencies + * + * @param Data $persistenceDataHelper + * @param PersistentSession $persistenceSessionHelper + * @param CustomerSession $customerSession + * @param QuoteManager $quoteManager + */ + public function __construct( + Data $persistenceDataHelper, + PersistentSession $persistenceSessionHelper, + CustomerSession $customerSession, + QuoteManager $quoteManager + ) { + $this->persistenceDataHelper = $persistenceDataHelper; + $this->persistenceSessionHelper = $persistenceSessionHelper; + $this->customerSession = $customerSession; + $this->quoteManager = $quoteManager; + } + + /** + * Convert shopping cart from persistent cart to guest cart after shipping information saved + * + * Check if shopping cart is persistent and customer is not logged in, and only one payment method is available, + * then converts the shopping cart guest cart. + * If only one payment is available, it's preselected by default and the payment information is automatically saved. + * + * @param GuestShippingInformationManagement $subject + * @param PaymentDetailsInterface $result + * @return PaymentDetailsInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSaveAddressInformation( + GuestShippingInformationManagement $subject, + PaymentDetailsInterface $result + ): PaymentDetailsInterface { + if ($this->persistenceSessionHelper->isPersistent() + && !$this->customerSession->isLoggedIn() + && $this->persistenceDataHelper->isShoppingCartPersist() + && $this->quoteManager->isPersistent() + && count($result->getPaymentMethods()) === 1 + ) { + $this->customerSession->setCustomerId(null); + $this->customerSession->setCustomerGroupId(null); + $this->quoteManager->convertCustomerCartToGuest(); + } + return $result; + } +} diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index ebddfe5a43600..b6504d528fbe4 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -5,8 +5,17 @@ */ namespace Magento\Persistent\Model; +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Persistent\Helper\Data; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartExtensionFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor; + /** - * Class QuoteManager + * Quote manager model * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ @@ -29,7 +38,7 @@ class QuoteManager /** * Persistent data * - * @var \Magento\Persistent\Helper\Data + * @var Data */ protected $persistentData; @@ -41,26 +50,44 @@ class QuoteManager protected $_setQuotePersistent = true; /** - * @var \Magento\Quote\Api\CartRepositoryInterface + * @var CartRepositoryInterface */ protected $quoteRepository; + /** + * @var ShippingAssignmentProcessor + */ + private $shippingAssignmentProcessor; + + /** + * @var CartExtensionFactory + */ + private $cartExtensionFactory; + /** * @param \Magento\Persistent\Helper\Session $persistentSession - * @param \Magento\Persistent\Helper\Data $persistentData + * @param Data $persistentData * @param \Magento\Checkout\Model\Session $checkoutSession - * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository + * @param CartRepositoryInterface $quoteRepository + * @param CartExtensionFactory|null $cartExtensionFactory + * @param ShippingAssignmentProcessor|null $shippingAssignmentProcessor */ public function __construct( \Magento\Persistent\Helper\Session $persistentSession, - \Magento\Persistent\Helper\Data $persistentData, + Data $persistentData, \Magento\Checkout\Model\Session $checkoutSession, - \Magento\Quote\Api\CartRepositoryInterface $quoteRepository + CartRepositoryInterface $quoteRepository, + ?CartExtensionFactory $cartExtensionFactory = null, + ?ShippingAssignmentProcessor $shippingAssignmentProcessor = null ) { $this->persistentSession = $persistentSession; $this->persistentData = $persistentData; $this->checkoutSession = $checkoutSession; $this->quoteRepository = $quoteRepository; + $this->cartExtensionFactory = $cartExtensionFactory + ?? ObjectManager::getInstance()->get(CartExtensionFactory::class); + $this->shippingAssignmentProcessor = $shippingAssignmentProcessor + ?? ObjectManager::getInstance()->get(ShippingAssignmentProcessor::class); } /** @@ -71,7 +98,7 @@ public function __construct( */ public function setGuest($checkQuote = false) { - /** @var $quote \Magento\Quote\Model\Quote */ + /** @var $quote Quote */ $quote = $this->checkoutSession->getQuote(); if ($quote && $quote->getId()) { if ($checkQuote && !$this->persistentData->isShoppingCartPersist() && !$quote->getIsPersistent()) { @@ -87,17 +114,19 @@ public function setGuest($checkQuote = false) ->setCustomerEmail(null) ->setCustomerFirstname(null) ->setCustomerLastname(null) - ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID) + ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID) ->setIsPersistent(false) ->removeAllAddresses(); //Create guest addresses $quote->getShippingAddress(); $quote->getBillingAddress(); + $this->setShippingAssignments($quote); $quote->collectTotals(); $this->quoteRepository->save($quote); } $this->persistentSession->getSession()->removePersistentCookie(); + $this->persistentSession->setSession(null); } /** @@ -111,7 +140,7 @@ public function setGuest($checkQuote = false) public function convertCustomerCartToGuest() { $quoteId = $this->checkoutSession->getQuoteId(); - /** @var $quote \Magento\Quote\Model\Quote */ + /** @var $quote Quote */ $quote = $this->quoteRepository->get($quoteId); if ($quote && $quote->getId()) { $this->_setQuotePersistent = false; @@ -126,6 +155,7 @@ public function convertCustomerCartToGuest() $quote->getAddressesCollection()->walk('setEmail', ['email' => null]); $quote->collectTotals(); $this->persistentSession->getSession()->removePersistentCookie(); + $this->persistentSession->setSession(null); $this->quoteRepository->save($quote); } } @@ -144,7 +174,7 @@ public function expire() $quote->setIsActive(true) ->setIsPersistent(false) ->setCustomerId(null) - ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); + ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID); } } @@ -157,4 +187,23 @@ public function isPersistent() { return $this->_setQuotePersistent; } + + /** + * Create shipping assignment for shopping cart + * + * @param CartInterface $quote + */ + private function setShippingAssignments(CartInterface $quote): void + { + $shippingAssignments = []; + if (!$quote->isVirtual() && $quote->getItemsQty() > 0) { + $shippingAssignments[] = $this->shippingAssignmentProcessor->create($quote); + } + $cartExtension = $quote->getExtensionAttributes(); + if ($cartExtension === null) { + $cartExtension = $this->cartExtensionFactory->create(); + } + $cartExtension->setShippingAssignments($shippingAssignments); + $quote->setExtensionAttributes($cartExtension); + } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestShippingInformationManagementPluginTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestShippingInformationManagementPluginTest.php new file mode 100644 index 0000000000000..8712068d4f3c8 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestShippingInformationManagementPluginTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Test\Unit\Model\Checkout; + +use Magento\Checkout\Api\Data\PaymentDetailsInterface; +use Magento\Checkout\Model\GuestShippingInformationManagement; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Persistent\Helper\Data; +use Magento\Persistent\Helper\Session as PersistenceSession; +use Magento\Persistent\Model\Checkout\GuestShippingInformationManagementPlugin; +use Magento\Persistent\Model\QuoteManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class GuestShippingInformationManagementPluginTest extends TestCase +{ + /** + * @var Data|MockObject + */ + private $persistenceDataHelper; + + /** + * @var PersistenceSession|MockObject + */ + private $persistenceSessionHelper; + + /** + * @var CustomerSession|MockObject + */ + private $customerSession; + + /** + * @var QuoteManager|MockObject + */ + private $quoteManager; + + /** + * @var GuestShippingInformationManagementPlugin + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->persistenceDataHelper = $this->createMock(Data::class); + $this->persistenceSessionHelper = $this->createMock(PersistenceSession::class); + $this->customerSession = $this->createMock(CustomerSession::class); + $this->quoteManager = $this->createMock(QuoteManager::class); + $this->model = new GuestShippingInformationManagementPlugin( + $this->persistenceDataHelper, + $this->persistenceSessionHelper, + $this->customerSession, + $this->quoteManager + ); + } + + /** + * @param array $paymentMethods + * @param bool $isLoggedIn + * @param bool $isPersistentSessionEnabled + * @param bool $isPersistentCartEnabled + * @param bool $isCartPersistent + * @param bool $isCartConverted + * @dataProvider afterSaveAddressInformationDataProvider + */ + public function testAfterSaveAddressInformation( + array $paymentMethods, + bool $isLoggedIn, + bool $isPersistentSessionEnabled, + bool $isPersistentCartEnabled, + bool $isCartPersistent, + bool $isCartConverted + ): void { + $subject = $this->createMock(GuestShippingInformationManagement::class); + $result = $this->createMock(PaymentDetailsInterface::class); + $result->method('getPaymentMethods') + ->willReturn($paymentMethods); + $this->customerSession->method('isLoggedIn') + ->willReturn($isLoggedIn); + $this->persistenceSessionHelper->method('isPersistent') + ->willReturn($isPersistentSessionEnabled); + $this->persistenceDataHelper->method('isShoppingCartPersist') + ->willReturn($isPersistentCartEnabled); + $this->quoteManager->method('isPersistent') + ->willReturn($isCartPersistent); + $this->customerSession->expects($this->exactly($isCartConverted ? 1 : 0)) + ->method('setCustomerId') + ->with(null); + $this->customerSession->expects($this->exactly($isCartConverted ? 1 : 0)) + ->method('setCustomerGroupId') + ->with(null); + $this->quoteManager->expects($this->exactly($isCartConverted ? 1 : 0)) + ->method('convertCustomerCartToGuest'); + $this->assertSame($result, $this->model->afterSaveAddressInformation($subject, $result)); + } + + /** + * @return array + */ + public function afterSaveAddressInformationDataProvider(): array + { + return [ + [['paypal'], false, true, true, true, true], + [['paypal'], true, true, true, true, false], + [['paypal', 'money_order'], false, true, true, true, false], + [['paypal', 'money_order'], true, true, true, true, false], + ]; + } +} diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index 6b8fe128cb191..5c4a3eb624d3c 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -8,17 +8,24 @@ namespace Magento\Persistent\Test\Unit\Model; +use Magento\Checkout\Model\Session; use Magento\Customer\Model\GroupManagement; use Magento\Eav\Model\Entity\Collection\AbstractCollection; use Magento\Persistent\Helper\Data; -use Magento\Persistent\Helper\Session; use Magento\Persistent\Model\QuoteManager; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartExtensionFactory; +use Magento\Quote\Api\Data\CartExtensionInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class QuoteManagerTest extends TestCase { /** @@ -27,7 +34,7 @@ class QuoteManagerTest extends TestCase protected $model; /** - * @var Session|MockObject + * @var \Magento\Persistent\Helper\Session|MockObject */ protected $persistentSessionMock; @@ -37,7 +44,7 @@ class QuoteManagerTest extends TestCase protected $persistentDataMock; /** - * @var \Magento\Checkout\Model\Session|MockObject + * @var Session|MockObject */ protected $checkoutSessionMock; @@ -61,9 +68,19 @@ class QuoteManagerTest extends TestCase */ protected $quoteRepositoryMock; + /** + * @var CartExtensionFactory|MockObject + */ + private $cartExtensionFactory; + + /** + * @var ShippingAssignmentProcessor|MockObject + */ + private $shippingAssignmentProcessor; + protected function setUp(): void { - $this->persistentSessionMock = $this->createMock(Session::class); + $this->persistentSessionMock = $this->createMock(\Magento\Persistent\Helper\Session::class); $this->sessionMock = $this->getMockBuilder(\Magento\Persistent\Model\Session::class)->addMethods([ 'setLoadInactive', @@ -76,7 +93,7 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); $this->persistentDataMock = $this->createMock(Data::class); - $this->checkoutSessionMock = $this->createMock(\Magento\Checkout\Model\Session::class); + $this->checkoutSessionMock = $this->createMock(Session::class); $this->abstractCollectionMock = $this->createMock(AbstractCollection::class); @@ -103,16 +120,25 @@ protected function setUp(): void 'collectTotals', 'removeAllAddresses', 'getIsActive', + 'isVirtual', + 'getItemsQty', + 'getExtensionAttributes', + 'setExtensionAttributes', '__wakeup' ]) ->disableOriginalConstructor() ->getMock(); + $this->cartExtensionFactory = $this->createPartialMock(CartExtensionFactory::class, ['create']); + $this->shippingAssignmentProcessor = $this->createPartialMock(ShippingAssignmentProcessor::class, ['create']); + $this->model = new QuoteManager( $this->persistentSessionMock, $this->persistentDataMock, $this->checkoutSessionMock, - $this->quoteRepositoryMock + $this->quoteRepositoryMock, + $this->cartExtensionFactory, + $this->shippingAssignmentProcessor ); } @@ -200,7 +226,32 @@ public function testSetGuest() ->method('getSession')->willReturn($this->sessionMock); $this->sessionMock->expects($this->once()) ->method('removePersistentCookie')->willReturn($this->sessionMock); - + $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn(false); + $this->quoteMock->expects($this->once())->method('getItemsQty')->willReturn(1); + $extensionAttributes = $this->createPartialMock( + CartExtensionInterface::class, + [ + 'setShippingAssignments', + 'getShippingAssignments' + ] + ); + $shippingAssignment = $this->createMock(ShippingAssignmentInterface::class); + $extensionAttributes->expects($this->once()) + ->method('setShippingAssignments') + ->with([$shippingAssignment]); + $this->shippingAssignmentProcessor->expects($this->once()) + ->method('create') + ->with($this->quoteMock) + ->willReturn($shippingAssignment); + $this->cartExtensionFactory->expects($this->once()) + ->method('create') + ->willReturn($extensionAttributes); + $this->quoteMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn(null); + $this->quoteMock->expects($this->once()) + ->method('setExtensionAttributes') + ->with($extensionAttributes); $this->model->setGuest(false); } diff --git a/app/code/Magento/Persistent/etc/frontend/di.xml b/app/code/Magento/Persistent/etc/frontend/di.xml index fae706fcc5808..3351963231277 100644 --- a/app/code/Magento/Persistent/etc/frontend/di.xml +++ b/app/code/Magento/Persistent/etc/frontend/di.xml @@ -52,4 +52,9 @@ <type name="Magento\Framework\App\Http\Context"> <plugin name="persistent_page_cache_variation" type="Magento\Persistent\Model\Plugin\PersistentCustomerContext" /> </type> + <type name="Magento\Persistent\Model\QuoteManager"> + <arguments> + <argument name="shippingAssignmentProcessor" xsi:type="object">Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor\Proxy</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/webapi_rest/di.xml b/app/code/Magento/Persistent/etc/webapi_rest/di.xml index e955dd81b1993..cb0aec6b460af 100644 --- a/app/code/Magento/Persistent/etc/webapi_rest/di.xml +++ b/app/code/Magento/Persistent/etc/webapi_rest/di.xml @@ -9,4 +9,8 @@ <type name="Magento\Checkout\Model\GuestPaymentInformationManagement"> <plugin name="inject_guest_address_for_nologin" type="Magento\Persistent\Model\Checkout\GuestPaymentInformationManagementPlugin" /> </type> + <type name="Magento\Checkout\Model\GuestShippingInformationManagement"> + <plugin name="persistent_convert_customer_cart_to_guest_cart" + type="Magento\Persistent\Model\Checkout\GuestShippingInformationManagementPlugin"/> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/webapi_soap/di.xml b/app/code/Magento/Persistent/etc/webapi_soap/di.xml index e955dd81b1993..cb0aec6b460af 100644 --- a/app/code/Magento/Persistent/etc/webapi_soap/di.xml +++ b/app/code/Magento/Persistent/etc/webapi_soap/di.xml @@ -9,4 +9,8 @@ <type name="Magento\Checkout\Model\GuestPaymentInformationManagement"> <plugin name="inject_guest_address_for_nologin" type="Magento\Persistent\Model\Checkout\GuestPaymentInformationManagementPlugin" /> </type> + <type name="Magento\Checkout\Model\GuestShippingInformationManagement"> + <plugin name="persistent_convert_customer_cart_to_guest_cart" + type="Magento\Persistent\Model\Checkout\GuestShippingInformationManagementPlugin"/> + </type> </config> 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/QuoteAnalytics/etc/reports.xml b/app/code/Magento/QuoteAnalytics/etc/reports.xml index f57012df23389..325901189dc20 100644 --- a/app/code/Magento/QuoteAnalytics/etc/reports.xml +++ b/app/code/Magento/QuoteAnalytics/etc/reports.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Analytics:etc/reports.xsd"> - <report name="quotes" connection="default"> + <report name="quotes" connection="checkout"> <source name="quote"> <attribute name="entity_id"/> <attribute name="customer_id"/> 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/Block/Adminhtml/Order/Create/Sidebar/Compared.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php index b4f2e132f6de4..e81d1f9589405 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php @@ -46,23 +46,15 @@ public function getItemCollection() $collection = $this->getData('item_collection'); if ($collection === null) { if ($collection = $this->getCreateOrderModel()->getCustomerCompareList()) { - $collection = $collection->getItemCollection()->useProductItem( - true - )->setStoreId( - $this->getQuote()->getStoreId() - )->addStoreFilter( - $this->getQuote()->getStoreId() - )->setCustomerId( - $this->getCustomerId() - )->addAttributeToSelect( - 'name' - )->addAttributeToSelect( - 'price' - )->addAttributeToSelect( - 'image' - )->addAttributeToSelect( - 'status' - )->load(); + $collection = $collection->getItemCollection() + ->useProductItem() + ->setStoreId($this->getQuote()->getStoreId()) + ->addStoreFilter($this->getQuote()->getStoreId()) + ->setCustomerId($this->getCustomerId()) + ->addAttributeToSelect('name') + ->addAttributeToSelect('price')->addAttributeToSelect('image') + ->addAttributeToSelect('status') + ->load(); } $this->setData('item_collection', $collection); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php index 8442d5b36466e..c9f251621f9de 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php @@ -89,13 +89,11 @@ public function getItemCollection() // get products to skip $skipProducts = []; if ($collection = $this->getCreateOrderModel()->getCustomerCompareList()) { - $collection = $collection->getItemCollection()->useProductItem( - true - )->setStoreId( - $this->getStoreId() - )->setCustomerId( - $this->getCustomerId() - )->load(); + $collection = $collection->getItemCollection() + ->useProductItem() + ->setStoreId($this->getStoreId()) + ->setCustomerId($this->getCustomerId()) + ->load(); foreach ($collection as $_item) { $skipProducts[] = $_item->getProductId(); } diff --git a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php index a3242228b28e0..978aec1b79ec4 100644 --- a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php +++ b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php @@ -5,12 +5,15 @@ */ namespace Magento\Sales\Cron; -use Magento\Quote\Model\ResourceModel\Quote\Collection; +use Exception; +use Magento\Quote\Model\QuoteRepository; +use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection; use Magento\Sales\Model\ResourceModel\Collection\ExpiredQuotesCollection; use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** - * Class CleanExpiredQuotes + * Cron job for cleaning expired Quotes */ class CleanExpiredQuotes { @@ -24,16 +27,32 @@ class CleanExpiredQuotes */ private $storeManager; + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param StoreManagerInterface $storeManager * @param ExpiredQuotesCollection $expiredQuotesCollection + * @param QuoteRepository $quoteRepository + * @param LoggerInterface $logger */ public function __construct( StoreManagerInterface $storeManager, - ExpiredQuotesCollection $expiredQuotesCollection + ExpiredQuotesCollection $expiredQuotesCollection, + QuoteRepository $quoteRepository, + LoggerInterface $logger ) { $this->storeManager = $storeManager; $this->expiredQuotesCollection = $expiredQuotesCollection; + $this->quoteRepository = $quoteRepository; + $this->logger = $logger; } /** @@ -45,9 +64,41 @@ public function execute() { $stores = $this->storeManager->getStores(true); foreach ($stores as $store) { - /** @var $quotes Collection */ - $quotes = $this->expiredQuotesCollection->getExpiredQuotes($store); - $quotes->walk('delete'); + /** @var $quoteCollection QuoteCollection */ + $quoteCollection = $this->expiredQuotesCollection->getExpiredQuotes($store); + $quoteCollection->setPageSize(50); + + // Last page returns 1 even when we don't have any results + $lastPage = $quoteCollection->getSize() ? $quoteCollection->getLastPageNumber() : 0; + + for ($currentPage = $lastPage; $currentPage >= 1; $currentPage--) { + $quoteCollection->setCurPage($currentPage); + + $this->deleteQuotes($quoteCollection); + } } } + + /** + * Deletes all quotes in collection + * + * @param QuoteCollection $quoteCollection + */ + private function deleteQuotes(QuoteCollection $quoteCollection): void + { + foreach ($quoteCollection as $quote) { + try { + $this->quoteRepository->delete($quote); + } catch (Exception $e) { + $message = sprintf( + 'Unable to delete expired quote (ID: %s): %s', + $quote->getId(), + (string)$e + ); + $this->logger->error($message); + } + } + + $quoteCollection->clear(); + } } 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/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml index 0888132669177..1c67d778937d1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml @@ -10,8 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontCreateOrdersWithMoveJSCodeBottomTest"> <annotations> + <stories value="Create a product and orders with set 'Move Js code to the bottom' to 'Yes'."/> <title value="Create a product and orders with set 'Move Js code to the bottom' to 'Yes'."/> <description value="Create a product and orders with a set 'Move JS code to the bottom of the page' to 'Yes' for registered customers and guests."/> + <severity value="MAJOR"/> </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableMoveJsCodeBottom.path}} {{StorefrontEnableMoveJsCodeBottom.value}}" stepKey="moveJsCodeBottomEnable"/> 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/Helper/CartFixedDiscount.php b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php new file mode 100644 index 0000000000000..b1fda3e78672b --- /dev/null +++ b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php @@ -0,0 +1,223 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Helper; + +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Model\Quote; +use Magento\SalesRule\Model\DeltaPriceRound; +use Magento\SalesRule\Model\Rule; + +/** + * Helper for CartFixed Available Discount and Quote Totals + */ +class CartFixedDiscount +{ + /** + * @var DeltaPriceRound + */ + private $deltaPriceRound; + + /** + * @var PriceCurrencyInterface + */ + private $priceCurrency; + + /** + * @param DeltaPriceRound $deltaPriceRound + * @param PriceCurrencyInterface $priceCurrency + */ + public function __construct( + DeltaPriceRound $deltaPriceRound, + PriceCurrencyInterface $priceCurrency + ) { + $this->deltaPriceRound = $deltaPriceRound; + $this->priceCurrency = $priceCurrency; + } + + /** + * Retrieve shipping amount by quote address and shipping method + * + * @param AddressInterface $address + * @return float + */ + public function calculateShippingAmountWhenAppliedToShipping( + AddressInterface $address + ): float { + $shippingAmount = (float) $address->getShippingAmount(); + if ($shippingAmount == 0.0) { + $address->setCollectShippingRates(true); + $address->collectShippingRates(); + $shippingRates = $address->getAllShippingRates(); + foreach ($shippingRates as $shippingRate) { + if ($shippingRate->getCode() === $address->getShippingMethod() + ) { + $shippingAmount = (float) $shippingRate->getPrice(); + break; + } + } + } + return $shippingAmount; + } + + /** + * Get the available discount amount calculated from the ration + * + * @param float $ruleDiscount + * @param float $qty + * @param float $baseItemPrice + * @param float $baseRuleTotals + * @param string $discountType + * @return float + */ + public function getDiscountAmount( + float $ruleDiscount, + float $qty, + float $baseItemPrice, + float $baseRuleTotals, + string $discountType + ): float { + $ratio = $baseItemPrice * $qty / $baseRuleTotals; + return $this->deltaPriceRound->round( + $ruleDiscount * $ratio, + $discountType + ); + } + + /** + * Get shipping discount amount + * + * @param Rule $rule + * @param float $shippingAmount + * @param float $quoteBaseSubtotal + * @return float + */ + public function getShippingDiscountAmount( + Rule $rule, + float $shippingAmount, + float $quoteBaseSubtotal + ): float { + $ratio = $shippingAmount / $quoteBaseSubtotal; + return $this->priceCurrency + ->roundPrice( + $rule->getDiscountAmount() * $ratio + ); + } + + /** + * Check if the current quote is multi shipping or not + * + * @param Quote $quote + * @return bool + */ + public function checkMultiShippingQuote(Quote $quote): bool + { + $isMultiShipping = false; + $extensionAttributes = $quote->getExtensionAttributes(); + if (!$quote->isVirtual() && + $extensionAttributes && + $extensionAttributes->getShippingAssignments()) { + $shippingAssignments = $extensionAttributes->getShippingAssignments(); + if (count($shippingAssignments) > 1) { + $isMultiShipping = true; + } + } + return $isMultiShipping; + } + + /** + * Get base rule totals for multi shipping addresses + * + * @param Quote $quote + * @return float + */ + public function getQuoteTotalsForMultiShipping(Quote $quote): float + { + $quoteTotal = $quote->getBaseSubtotal(); + $extensionAttributes = $quote->getExtensionAttributes(); + $shippingAssignments = $extensionAttributes->getShippingAssignments(); + $totalShippingPrice = 0.0; + foreach ($shippingAssignments as $assignment) { + $totalShippingPrice += $assignment->getShipping()->getAddress()->getBaseShippingInclTax(); + } + return $quoteTotal + $totalShippingPrice; + } + + /** + * Get base rule totals for regular shipping address + * + * @param Quote\Address $address + * @param float $baseRuleTotals + * @return float + */ + public function getQuoteTotalsForRegularShipping( + Quote\Address $address, + float $baseRuleTotals + ): float { + $baseRuleTotals += $this->calculateShippingAmountWhenAppliedToShipping( + $address + ); + return $baseRuleTotals; + } + + /** + * Get base rule totals + * + * @param int $isAppliedToShipping + * @param Quote $quote + * @param bool $isMultiShipping + * @param Quote\Address $address + * @param float $baseRuleTotals + * @return float + */ + public function getBaseRuleTotals( + int $isAppliedToShipping, + Quote $quote, + bool $isMultiShipping, + Quote\Address $address, + float $baseRuleTotals + ): float { + if ($isAppliedToShipping) { + $baseRuleTotals = ($quote->getIsMultiShipping() && $isMultiShipping) ? + $this->getQuoteTotalsForMultiShipping($quote) : + $this->getQuoteTotalsForRegularShipping($address, $baseRuleTotals); + } else { + if ($quote->getIsMultiShipping() && $isMultiShipping) { + $baseRuleTotals = $quote->getBaseSubtotal(); + } + } + return (float) $baseRuleTotals; + } + + /** + * Get available discount amount + * + * @param Rule $rule + * @param Quote $quote + * @param bool $isMultiShipping + * @param array $cartRules + * @param float $baseDiscountAmount + * @param float $availableDiscountAmount + * @return float + */ + public function getAvailableDiscountAmount( + Rule $rule, + Quote $quote, + bool $isMultiShipping, + array $cartRules, + float $baseDiscountAmount, + float $availableDiscountAmount + ): float { + if ($quote->getIsMultiShipping() && $isMultiShipping) { + $availableDiscountAmount = (float)$cartRules[$rule->getId()] - $baseDiscountAmount; + } else { + $availableDiscountAmount -= $baseDiscountAmount; + } + return $availableDiscountAmount; + } +} diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php index e44200614fa00..b4585bb047c44 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -5,8 +5,13 @@ */ namespace Magento\SalesRule\Model\Rule\Action\Discount; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\SalesRule\Helper\CartFixedDiscount; use Magento\SalesRule\Model\DeltaPriceRound; +use Magento\SalesRule\Model\Rule; use Magento\SalesRule\Model\Validator; /** @@ -26,6 +31,11 @@ class CartFixed extends AbstractDiscount */ private $deltaPriceRound; + /** + * @var CartFixedDiscount + */ + private $cartFixedDiscountHelper; + /** * @var string */ @@ -36,35 +46,47 @@ class CartFixed extends AbstractDiscount * @param DataFactory $discountDataFactory * @param PriceCurrencyInterface $priceCurrency * @param DeltaPriceRound $deltaPriceRound + * @param CartFixedDiscount|null $cartFixedDiscount */ public function __construct( Validator $validator, DataFactory $discountDataFactory, PriceCurrencyInterface $priceCurrency, - DeltaPriceRound $deltaPriceRound + DeltaPriceRound $deltaPriceRound, + ?CartFixedDiscount $cartFixedDiscount = null ) { $this->deltaPriceRound = $deltaPriceRound; - + $this->cartFixedDiscountHelper = $cartFixedDiscount ?: + ObjectManager::getInstance()->get(CartFixedDiscount::class); parent::__construct($validator, $discountDataFactory, $priceCurrency); } /** * Fixed discount for cart calculation * - * @param \Magento\SalesRule\Model\Rule $rule - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * @param Rule $rule + * @param AbstractItem $item * @param float $qty - * @return \Magento\SalesRule\Model\Rule\Action\Discount\Data + * @return Data + * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function calculate($rule, $item, $qty) { - /** @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData */ + /** @var Data $discountData */ $discountData = $this->discountFactory->create(); $ruleTotals = $this->validator->getRuleItemTotalsInfo($rule->getId()); + $baseRuleTotals = $ruleTotals['base_items_price'] ?? 0.0; + $address = $item->getAddress(); + $shippingMethod = $address->getShippingMethod(); + $isAppliedToShipping = (int) $rule->getApplyToShipping(); $quote = $item->getQuote(); + $ruleDiscount = (float) $rule->getDiscountAmount(); + $isMultiShipping = $this->cartFixedDiscountHelper->checkMultiShippingQuote($quote); $itemPrice = $this->validator->getItemPrice($item); $baseItemPrice = $this->validator->getItemBasePrice($item); $itemOriginalPrice = $this->validator->getItemOriginalPrice($item); @@ -74,41 +96,91 @@ public function calculate($rule, $item, $qty) if (!isset($cartRules[$rule->getId()])) { $cartRules[$rule->getId()] = $rule->getDiscountAmount(); } - - $availableDiscountAmount = (float)$cartRules[$rule->getId()]; + $availableDiscountAmount = (float) $cartRules[$rule->getId()]; $discountType = self::$discountType . $rule->getId(); if ($availableDiscountAmount > 0) { $store = $quote->getStore(); if ($ruleTotals['items_count'] <= 1) { + $baseRuleTotals = $shippingMethod ? + $this->cartFixedDiscountHelper + ->getBaseRuleTotals( + $isAppliedToShipping, + $quote, + $isMultiShipping, + $address, + $baseRuleTotals + ) : $baseRuleTotals; + $availableDiscountAmount = $this->cartFixedDiscountHelper + ->getDiscountAmount( + $ruleDiscount, + $qty, + $baseItemPrice, + $baseRuleTotals, + $discountType + ); $quoteAmount = $this->priceCurrency->convert($availableDiscountAmount, $store); $baseDiscountAmount = min($baseItemPrice * $qty, $availableDiscountAmount); $this->deltaPriceRound->reset($discountType); } else { - $ratio = $baseItemPrice * $qty / $ruleTotals['base_items_price']; - $maximumItemDiscount = $this->deltaPriceRound->round( - $rule->getDiscountAmount() * $ratio, - $discountType - ); - + $baseRuleTotals = $shippingMethod ? + $this->cartFixedDiscountHelper + ->getBaseRuleTotals( + $isAppliedToShipping, + $quote, + $isMultiShipping, + $address, + $baseRuleTotals + ) : $baseRuleTotals; + $maximumItemDiscount =$this->cartFixedDiscountHelper + ->getDiscountAmount( + $ruleDiscount, + $qty, + $baseItemPrice, + $baseRuleTotals, + $discountType + ); $quoteAmount = $this->priceCurrency->convert($maximumItemDiscount, $store); - $baseDiscountAmount = min($baseItemPrice * $qty, $maximumItemDiscount); $this->validator->decrementRuleItemTotalsCount($rule->getId()); } - $baseDiscountAmount = $this->priceCurrency->round($baseDiscountAmount); + $baseDiscountAmount = $this->priceCurrency->roundPrice($baseDiscountAmount); - $availableDiscountAmount -= $baseDiscountAmount; + $availableDiscountAmount = $this->cartFixedDiscountHelper + ->getAvailableDiscountAmount( + $rule, + $quote, + $isMultiShipping, + $cartRules, + $baseDiscountAmount, + $availableDiscountAmount + ); $cartRules[$rule->getId()] = $availableDiscountAmount; + if ($isAppliedToShipping && + $isMultiShipping && + $ruleTotals['items_count'] <= 1) { + $estimatedShippingAmount = (float) $address->getBaseShippingInclTax(); + $shippingDiscountAmount = $this->cartFixedDiscountHelper-> + getShippingDiscountAmount( + $rule, + $estimatedShippingAmount, + $baseRuleTotals + ); + $cartRules[$rule->getId()] -= $shippingDiscountAmount; + if ($cartRules[$rule->getId()] < 0.0) { + $baseDiscountAmount += $cartRules[$rule->getId()]; + $quoteAmount += $cartRules[$rule->getId()]; + } + } if ($availableDiscountAmount <= 0) { $this->deltaPriceRound->reset($discountType); } - $discountData->setAmount($this->priceCurrency->round(min($itemPrice * $qty, $quoteAmount))); + $discountData->setAmount($this->priceCurrency->roundPrice(min($itemPrice * $qty, $quoteAmount))); $discountData->setBaseAmount($baseDiscountAmount); $discountData->setOriginalAmount(min($itemOriginalPrice * $qty, $quoteAmount)); - $discountData->setBaseOriginalAmount($this->priceCurrency->round($baseItemOriginalPrice)); + $discountData->setBaseOriginalAmount($this->priceCurrency->roundPrice($baseItemOriginalPrice)); } $quote->setCartFixedRules($cartRules); diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 06bc7e12fb973..0fc0b062c7887 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -6,8 +6,11 @@ namespace Magento\SalesRule\Model; +use Magento\Framework\App\ObjectManager; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\SalesRule\Helper\CartFixedDiscount; +use Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory; /** * SalesRule Validator Model @@ -61,7 +64,7 @@ class Validator extends \Magento\Framework\Model\AbstractModel protected $_catalogData = null; /** - * @var \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory + * @var CollectionFactory */ protected $_collectionFactory; @@ -97,10 +100,15 @@ class Validator extends \Magento\Framework\Model\AbstractModel */ protected $counter = 0; + /** + * @var CartFixedDiscount + */ + private $cartFixedDiscountHelper; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry - * @param \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory $collectionFactory + * @param CollectionFactory $collectionFactory * @param \Magento\Catalog\Helper\Data $catalogData * @param Utility $utility * @param RulesApplier $rulesApplier @@ -110,12 +118,13 @@ class Validator extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param CartFixedDiscount|null $cartFixedDiscount * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, \Magento\Framework\Registry $registry, - \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory $collectionFactory, + CollectionFactory $collectionFactory, \Magento\Catalog\Helper\Data $catalogData, \Magento\SalesRule\Model\Utility $utility, \Magento\SalesRule\Model\RulesApplier $rulesApplier, @@ -124,7 +133,8 @@ public function __construct( \Magento\Framework\Message\ManagerInterface $messageManager, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ?CartFixedDiscount $cartFixedDiscount = null ) { $this->_collectionFactory = $collectionFactory; $this->_catalogData = $catalogData; @@ -133,6 +143,8 @@ public function __construct( $this->priceCurrency = $priceCurrency; $this->validators = $validators; $this->messageManager = $messageManager; + $this->cartFixedDiscountHelper = $cartFixedDiscount ?: + ObjectManager::getInstance()->get(CartFixedDiscount::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -158,6 +170,7 @@ public function init($websiteId, $customerGroupId, $couponCode) * * @param Address|null $address * @return \Magento\SalesRule\Model\ResourceModel\Rule\Collection + * @throws \Zend_Db_Select_Exception */ protected function _getRules(Address $address = null) { @@ -221,6 +234,7 @@ public function setSkipActionsValidation($flag) * * @param AbstractItem $item * @return bool + * @throws \Zend_Db_Select_Exception */ public function canApplyRules(AbstractItem $item) { @@ -259,6 +273,7 @@ public function reset(Address $address) * * @param AbstractItem $item * @return $this + * @throws \Zend_Db_Select_Exception */ public function process(AbstractItem $item) { @@ -288,6 +303,9 @@ public function process(AbstractItem $item) * @param Address $address * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Zend_Db_Select_Exception */ public function processShippingAmount(Address $address) { @@ -313,6 +331,7 @@ public function processShippingAmount(Address $address) case \Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION: $rulePercent = max(0, 100 - $rule->getDiscountAmount()); // break is intentionally omitted + // no break case \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION: $discountAmount = ($shippingAmount - $address->getShippingDiscountAmount()) * $rulePercent / 100; $baseDiscountAmount = ($baseShippingAmount - @@ -331,7 +350,40 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = $rule->getDiscountAmount(); break; case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION: - // Shouldn't be proceed according to MAGETWO-96403 + $cartRules = $address->getCartFixedRules(); + $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore()); + $isAppliedToShipping = (int) $rule->getApplyToShipping(); + if (!isset($cartRules[$rule->getId()])) { + $cartRules[$rule->getId()] = $rule->getDiscountAmount(); + } + if ($cartRules[$rule->getId()] > 0) { + $shippingAmount = $address->getShippingAmount() - $address->getShippingDiscountAmount(); + $quoteBaseSubtotal = (float) $quote->getBaseSubtotal(); + $isMultiShipping = $this->cartFixedDiscountHelper->checkMultiShippingQuote($quote); + if ($isAppliedToShipping) { + $quoteBaseSubtotal = ($quote->getIsMultiShipping() && $isMultiShipping) ? + $this->cartFixedDiscountHelper->getQuoteTotalsForMultiShipping($quote) : + $this->cartFixedDiscountHelper->getQuoteTotalsForRegularShipping( + $address, + $quoteBaseSubtotal + ); + $discountAmount = $this->cartFixedDiscountHelper-> + getShippingDiscountAmount( + $rule, + $shippingAmount, + $quoteBaseSubtotal + ); + $baseDiscountAmount = $discountAmount; + } else { + $discountAmount = min($shippingAmount, $quoteAmount); + $baseDiscountAmount = min( + $baseShippingAmount - $address->getBaseShippingDiscountAmount(), + $cartRules[$rule->getId()] + ); + } + $cartRules[$rule->getId()] -= $baseDiscountAmount; + } + $address->setCartFixedRules($cartRules); break; } @@ -363,6 +415,8 @@ public function processShippingAmount(Address $address) * @param mixed $items * @param Address $address * @return $this + * @throws \Zend_Validate_Exception + * @throws \Zend_Db_Select_Exception */ public function initTotals($items, Address $address) { @@ -505,6 +559,7 @@ public function prepareDescription($address, $separator = ', ') * @param array $items * @param Address $address * @return array $items + * @throws \Zend_Db_Select_Exception */ public function sortItemsByPriority($items, Address $address = null) { @@ -560,6 +615,7 @@ public function decrementRuleItemTotalsCount($key) * * @param AbstractItem $item * @return bool + * @throws \Zend_Validate_Exception */ public function canApplyDiscount(AbstractItem $item) { 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/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 24c3a7cd44bc8..9f4168575595a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -55,9 +55,11 @@ <click selector="{{AdminCartPriceRulesFormSection.generateCouponsButton}}" stepKey="clickGenerate"/> <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="Message is added to queue, wait to get your coupons soon" stepKey="seeGenerationSuccess"/> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <!-- Apply changes --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminCodeGeneratorMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminCodeGeneratorMessageConsumerData.messageLimit}}"/> + </actionGroup> <reloadPage stepKey="refreshPage"/> <waitForPageLoad stepKey="waitFormToReload1"/> <click selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" stepKey="expandCouponSection2"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index 09b45cd554056..b77cfaf02d232 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -59,9 +59,11 @@ <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="Message is added to queue, wait to get your coupons soon" stepKey="seeSuccessMessage"/> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <!-- Start message queue for export consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminCodeGeneratorMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminCodeGeneratorMessageConsumerData.messageLimit}}"/> + </actionGroup> <reloadPage stepKey="refreshPage"/> <waitForPageLoad stepKey="waitFormToReload1"/> <conditionalClick selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php index d10111568396d..276a308980683 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/CartFixedTest.php @@ -7,11 +7,13 @@ namespace Magento\SalesRule\Test\Unit\Model\Rule\Action\Discount; -use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Api\Data\CartExtensionInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\SalesRule\Helper\CartFixedDiscount; use Magento\SalesRule\Model\DeltaPriceRound; use Magento\SalesRule\Model\Rule; use Magento\SalesRule\Model\Rule\Action\Discount\CartFixed; @@ -24,6 +26,7 @@ /** * Tests for Magento\SalesRule\Model\Rule\Action\Discount\CartFixed. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CartFixedTest extends TestCase @@ -68,14 +71,23 @@ class CartFixedTest extends TestCase */ protected $priceCurrency; + /** + * @var DeltaPriceRound|MockObject + */ + protected $deltaPriceRound; + + /** + * @var CartFixedDiscount|MockObject + */ + protected $cartFixedDiscountHelper; + /** * @inheritdoc */ protected function setUp(): void { - $this->rule = $this->getMockBuilder(DataObject::class) - ->setMockClassName('Rule') - ->setMethods(null) + $this->rule = $this->getMockBuilder(Rule::class) + ->setMethods(['getId', 'getApplyToShipping']) ->disableOriginalConstructor() ->getMock(); $this->item = $this->createMock(AbstractItem::class); @@ -83,11 +95,12 @@ protected function setUp(): void $this->quote = $this->getMockBuilder(Quote::class) ->addMethods(['getCartFixedRules', 'setCartFixedRules']) - ->onlyMethods(['getStore']) + ->onlyMethods(['getStore', 'getExtensionAttributes', 'isVirtual']) ->disableOriginalConstructor() ->getMock(); - $this->address = $this->createMock( - Address::class + $this->address = $this->createPartialMock( + Address::class, + ['getShippingMethod'] ); $this->item->expects($this->any())->method('getQuote')->willReturn($this->quote); $this->item->expects($this->any())->method('getAddress')->willReturn($this->address); @@ -98,10 +111,24 @@ protected function setUp(): void DataFactory::class, ['create'] ); - $dataFactory->expects($this->any())->method('create')->willReturn($this->data); + $dataFactory->method('create')->willReturn($this->data); $this->priceCurrency = $this->getMockBuilder(PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->setMethods(['roundPrice']) + ->getMockForAbstractClass(); + $this->deltaPriceRound = $this->getMockBuilder(DeltaPriceRound::class) + ->setMethods(['round']) + ->disableOriginalConstructor() ->getMock(); - $deltaPriceRound = $this->getMockBuilder(DeltaPriceRound::class) + $this->cartFixedDiscountHelper = $this->getMockBuilder(CartFixedDiscount::class) + ->setMethods([ + 'calculateShippingAmountWhenAppliedToShipping', + 'getDiscountAmount', + 'checkMultiShippingQuote', + 'getQuoteTotalsForMultiShipping', + 'getQuoteTotalsForRegularShipping', + 'getBaseRuleTotals', + 'getAvailableDiscountAmount']) ->disableOriginalConstructor() ->getMock(); @@ -109,78 +136,170 @@ protected function setUp(): void $this->validator, $dataFactory, $this->priceCurrency, - $deltaPriceRound + $this->deltaPriceRound, + $this->cartFixedDiscountHelper ); } /** * @covers \Magento\SalesRule\Model\Rule\Action\Discount\CartFixed::calculate + * @dataProvider dataProviderActions + * @param array $shipping + * @param array $ruleDetails + * @throws LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testCalculate() + public function testCalculate(array $shipping, array $ruleDetails): void { - $ruleItemTotals = [ - 'items_price' => 100, - 'base_items_price' => 100, - 'items_count' => 1, - ]; + $this->rule->setData(['id' => $ruleDetails['id'], 'discount_amount' => $ruleDetails['discounted_amount']]); + $this->rule + ->expects($this->any()) + ->method('getId') + ->will( + $this->returnValue( + $ruleDetails['id'] + ) + ); + $this->rule + ->expects($this->any()) + ->method('getApplyToShipping') + ->will( + $this->returnValue( + $shipping['is_applied_to_shipping'] + ) + ); + $this->cartFixedDiscountHelper + ->expects($this->any()) + ->method('getDiscountAmount') + ->will( + $this->returnValue( + $ruleDetails['discounted_amount'] + ) + ); + $cartExtensionMock = $this->getMockBuilder(CartExtensionInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getShippingAssignments']) + ->getMockForAbstractClass(); + $this->quote->expects($this->any())->method('getCartFixedRules')->will($this->returnValue([])); + $store = $this->createMock(Store::class); + $this->priceCurrency + ->expects($this->atLeastOnce()) + ->method('convert') + ->willReturnArgument($ruleDetails['rounded_amount']); + $this->priceCurrency + ->expects($this->atLeastOnce()) + ->method('roundPrice') + ->willReturnArgument($ruleDetails['rounded_amount']); + $this->deltaPriceRound + ->expects($this->any()) + ->method('round') + ->willReturnArgument($ruleDetails['base_items_price']); + $this->quote->expects($this->any())->method('getStore')->will($this->returnValue($store)); + $this->quote->method('isVirtual') + ->willReturn(false); + $this->quote->method('getExtensionAttributes') + ->willReturn($cartExtensionMock); - $this->rule->setData(['id' => 1, 'discount_amount' => 10.0]); + $cartExtensionMock->method('getShippingAssignments') + ->willReturn($shipping['shipping_assignment']); - $this->quote->expects($this->any())->method('getCartFixedRules')->willReturn([]); - $store = $this->createMock(Store::class); - $this->priceCurrency->expects($this->atLeastOnce())->method('convert')->willReturnArgument(0); - $this->priceCurrency->expects($this->atLeastOnce())->method('round')->willReturnArgument(0); - $this->quote->expects($this->any())->method('getStore')->willReturn($store); + $this->address + ->expects($this->once()) + ->method('getShippingMethod') + ->will( + $this->returnValue( + $shipping['shipping_method'] + ) + ); - $this->validator->expects($this->once()) + /** validators data */ + $this->validator + ->expects($this->once()) + ->method('getItemPrice') + ->with($this->item) + ->will($this->returnValue($ruleDetails['items_price'])); + $this->validator + ->expects($this->once()) + ->method('getItemBasePrice') + ->with($this->item) + ->will($this->returnValue($ruleDetails['base_items_price'])); + $this->validator + ->expects($this->once()) + ->method('getItemOriginalPrice') + ->with($this->item) + ->will($this->returnValue($ruleDetails['items_price'])); + $this->validator + ->expects($this->once()) + ->method('getItemBaseOriginalPrice') + ->with($this->item) + ->will($this->returnValue($ruleDetails['items_price'])); + $this->validator + ->expects($this->once()) ->method('getRuleItemTotalsInfo') ->with($this->rule->getId()) - ->willReturn($ruleItemTotals); + ->will($this->returnValue($ruleDetails)); - /** validators data */ - $this->validator->expects( - $this->once() - )->method( - 'getItemPrice' - )->with( - $this->item - )->willReturn( - 100 - ); - $this->validator->expects( - $this->once() - )->method( - 'getItemBasePrice' - )->with( - $this->item - )->willReturn( - 100 - ); - $this->validator->expects( - $this->once() - )->method( - 'getItemOriginalPrice' - )->with( - $this->item - )->willReturn( - 100 - ); - $this->validator->expects( - $this->once() - )->method( - 'getItemBaseOriginalPrice' - )->with( - $this->item - )->willReturn( - 100 - ); + $this->quote->expects($this->once())->method('setCartFixedRules')->with([1 => $ruleDetails['cart_rules']]); + $this->model->calculate($this->rule, $this->item, $ruleDetails['items_count']); - $this->quote->expects($this->once())->method('setCartFixedRules')->with([1 => 0.0]); - $this->model->calculate($this->rule, $this->item, 1); + $this->assertEquals($this->data->getAmount(), $ruleDetails['base_items_price']); + $this->assertEquals($this->data->getBaseAmount(), $ruleDetails['base_items_price']); + $this->assertEquals($this->data->getOriginalAmount(), $ruleDetails['base_items_price']); + $this->assertEquals($this->data->getBaseOriginalAmount(), $ruleDetails['items_price']); + } + + /** + * @return array + */ + public static function dataProviderActions() + { + return [ + 'regular shipping with single item and single shipping' => [ + [ + 'shipping_method' => 'flatrate_flatrate', + 'is_applied_to_shipping' => 0, + 'shipping_assignment' => ['test_assignment_1'] + ], + [ 'id' => 1, + 'base_items_price' => 10.0, + 'items_price' => 100.0, + 'items_count' => 1, + 'rounded_amount' => 0.0, + 'discounted_amount' => 10.0, + 'cart_rules' => 0.0 + ] + ], + 'regular shipping with two items and single shipping' => [ + [ + 'shipping_method' => 'flatrate_flatrate', + 'is_applied_to_shipping' => 0, + 'shipping_assignment' => ['test_assignment_1'] + ], + [ 'id' => 1, + 'base_items_price' => 10.0, + 'items_price' => 100.0, + 'items_count' => 2, + 'rounded_amount' => 0.0, + 'discounted_amount' => 10.0, + 'cart_rules' => 0.0 + ] + ], + 'regular shipping with two items and multiple shipping' => [ + [ + 'shipping_method' => 'flatrate_flatrate', + 'is_applied_to_shipping' => 0, + 'shipping_assignment' => ['test_assignment_1', 'test_assignment_2'] + ], + [ 'id' => 1, + 'base_items_price' => 10.0, + 'items_price' => 200.0, + 'items_count' => 2, + 'rounded_amount' => 0.0, + 'discounted_amount' => 10.0, + 'cart_rules' => 0.0 + ] + ] - $this->assertEquals($this->data->getAmount(), 10); - $this->assertEquals($this->data->getBaseAmount(), 10); - $this->assertEquals($this->data->getOriginalAmount(), 10); - $this->assertEquals($this->data->getBaseOriginalAmount(), 100); + ]; } } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 8ba770e588924..4224cfafb3c8c 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -8,6 +8,7 @@ namespace Magento\SalesRule\Test\Unit\Model; use Magento\Catalog\Helper\Data; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Message\Manager; use Magento\Framework\Message\ManagerInterface; use Magento\Framework\Model\Context; @@ -15,11 +16,14 @@ use Magento\Framework\Registry; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Validator\AbstractValidator; +use Magento\Quote\Api\Data\CartExtensionInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Item; use Magento\Quote\Model\Quote\Item\AbstractItem; -use Magento\SalesRule\Model\ResourceModel\Rule\Collection; +use Magento\Rule\Model\Action\Collection; +use Magento\SalesRule\Helper\CartFixedDiscount; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection as RuleCollection; use Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory; use Magento\SalesRule\Model\Rule; use Magento\SalesRule\Model\RulesApplier; @@ -31,8 +35,9 @@ use PHPUnit\Framework\TestCase; /** - * Tests for Magento\SalesRule\Model\Validator - * @@SuppressWarnings(PHPMD.CouplingBetweenObjects) + * Test sales rule model validator + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ValidatorTest extends TestCase { @@ -72,7 +77,7 @@ class ValidatorTest extends TestCase protected $utility; /** - * @var Collection|MockObject + * @var RuleCollection|MockObject */ protected $ruleCollection; @@ -91,6 +96,11 @@ class ValidatorTest extends TestCase */ private $priceCurrency; + /** + * @var CartFixedDiscount|MockObject + */ + private $cartFixedDiscountHelper; + protected function setUp(): void { $this->helper = new ObjectManager($this); @@ -128,7 +138,7 @@ protected function setUp(): void $this->utility = $this->createMock(Utility::class); $this->validators = $this->createPartialMock(Pool::class, ['getValidators']); $this->messageManager = $this->createMock(Manager::class); - $this->ruleCollection = $this->getMockBuilder(Collection::class) + $this->ruleCollection = $this->getMockBuilder(RuleCollection::class) ->disableOriginalConstructor() ->getMock(); $ruleCollectionFactoryMock = $this->prepareRuleCollectionMock($this->ruleCollection); @@ -136,7 +146,18 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['roundPrice']) ->getMockForAbstractClass(); - + $this->cartFixedDiscountHelper = $this->getMockBuilder(CartFixedDiscount::class) + ->setMethods([ + 'calculateShippingAmountWhenAppliedToShipping', + 'getDiscountAmount', + 'getShippingDiscountAmount', + 'checkMultiShippingQuote', + 'getQuoteTotalsForMultiShipping', + 'getQuoteTotalsForRegularShipping', + 'getBaseRuleTotals', + 'getAvailableDiscountAmount']) + ->disableOriginalConstructor() + ->getMock(); /** @var Validator|MockObject $validator */ $this->model = $this->helper->getObject( Validator::class, @@ -149,7 +170,8 @@ protected function setUp(): void 'rulesApplier' => $this->rulesApplier, 'validators' => $this->validators, 'messageManager' => $this->messageManager, - 'priceCurrency' => $this->priceCurrency + 'priceCurrency' => $this->priceCurrency, + 'cartFixedDiscountHelper' => $this->cartFixedDiscountHelper ] ); $this->model->setWebsiteId(1); @@ -169,6 +191,7 @@ protected function setUp(): void /** * @return Item|MockObject + * @throws LocalizedException */ protected function getQuoteItemMock() { @@ -206,9 +229,8 @@ public function testCanApplyRules() ); $item = $this->getQuoteItemMock(); $rule = $this->createMock(Rule::class); - $actionsCollection = $this->getMockBuilder(\Magento\Rule\Model\Action\Collection::class)->addMethods( - ['validate'] - ) + $actionsCollection = $this->getMockBuilder(Collection::class) + ->addMethods(['validate']) ->disableOriginalConstructor() ->getMock(); $actionsCollection->expects($this->any()) @@ -414,9 +436,8 @@ public function testInitTotalsCanApplyDiscount() $this->utility->expects($this->once())->method('getItemQty')->willReturn(1); $this->utility->expects($this->any())->method('canProcessRule')->willReturn(true); - $actionsCollection = $this->getMockBuilder(\Magento\Rule\Model\Action\Collection::class)->addMethods( - ['validate'] - ) + $actionsCollection = $this->getMockBuilder(Collection::class) + ->addMethods(['validate']) ->disableOriginalConstructor() ->getMock(); $actionsCollection->expects($this->at(0))->method('validate')->with($item1)->willReturn(true); @@ -517,10 +538,12 @@ public function testProcessShippingAmountProcessDisabled() * @param int $ruleDiscount * @param int $shippingDiscount * @dataProvider dataProviderActions + * @throws \Zend_Db_Select_Exception */ public function testProcessShippingAmountActions($action, $ruleDiscount, $shippingDiscount): void { - $shippingAmount = 5; + $shippingAmount = 5.0; + $quoteBaseSubTotal = 10.0; $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() @@ -552,7 +575,7 @@ public function testProcessShippingAmountActions($action, $ruleDiscount, $shippi $this->model->getCouponCode() ); - $addressMock = $this->setupAddressMock($shippingAmount); + $addressMock = $this->setupAddressMock($shippingAmount, $quoteBaseSubTotal); self::assertInstanceOf(Validator::class, $this->model->processShippingAmount($addressMock)); self::assertEquals($shippingDiscount, $addressMock->getShippingDiscountAmount()); @@ -573,11 +596,13 @@ public static function dataProviderActions() } /** - * @param null|int $shippingAmount + * @param float $shippingAmount + * @param float $quoteBaseSubTotal * @return MockObject */ - protected function setupAddressMock($shippingAmount = null) + protected function setupAddressMock($shippingAmount = 0.0, $quoteBaseSubTotal = 0.0) { + $shippingAssignments = ['test_assignment_1']; $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->setMethods([]) @@ -585,8 +610,18 @@ protected function setupAddressMock($shippingAmount = null) $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() - ->setMethods(['setAppliedRuleIds', 'getStore']) + ->setMethods([ + 'setAppliedRuleIds', + 'getStore', + 'getBaseSubtotal', + 'getExtensionAttributes', + 'isVirtual' + ]) ->getMock(); + $cartExtensionMock = $this->getMockBuilder(CartExtensionInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getShippingAssignments']) + ->getMockForAbstractClass(); $quoteMock->method('getStore') ->willReturn($storeMock); @@ -594,6 +629,26 @@ protected function setupAddressMock($shippingAmount = null) $quoteMock->method('setAppliedRuleIds') ->willReturnSelf(); + $quoteMock->method('isVirtual') + ->willReturn(false); + + $quoteMock->method('getBaseSubtotal') + ->willReturn($quoteBaseSubTotal); + + $this->cartFixedDiscountHelper + ->method('getQuoteTotalsForRegularShipping') + ->willReturn($quoteBaseSubTotal); + + $this->cartFixedDiscountHelper + ->method('getShippingDiscountAmount') + ->willReturn($shippingAmount); + + $quoteMock->method('getExtensionAttributes') + ->willReturn($cartExtensionMock); + + $cartExtensionMock->method('getShippingAssignments') + ->willReturn($shippingAssignments); + $this->addressMock->method('getShippingAmountForDiscount') ->willReturn($shippingAmount); diff --git a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js index dfb3f909345b3..6868617c1c449 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js +++ b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js @@ -22,9 +22,6 @@ define([ */ toggleDisabled: function (action) { switch (action) { - case 'cart_fixed': - this.disabled(true); - break; default: this.disabled(false); } 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/Console/Command/SampleDataDeployCommand.php b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php index 57a61fecae5ca..76159dc8320e1 100644 --- a/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php +++ b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php @@ -7,63 +7,83 @@ namespace Magento\SampleData\Console\Command; use Composer\Console\Application; +use Composer\Console\ApplicationFactory; +use Exception; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\SampleData\Model\Dependency; use Magento\Setup\Model\PackagesAuth; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\ArrayInputFactory; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Command for deployment of Sample Data + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SampleDataDeployCommand extends Command { const OPTION_NO_UPDATE = 'no-update'; /** - * @var \Magento\Framework\Filesystem + * @var Filesystem */ private $filesystem; /** - * @var \Magento\SampleData\Model\Dependency + * @var Dependency */ private $sampleDataDependency; /** - * @var \Symfony\Component\Console\Input\ArrayInputFactory + * @var ArrayInputFactory * @deprecated 100.1.0 */ private $arrayInputFactory; /** - * @var \Composer\Console\ApplicationFactory + * @var ApplicationFactory */ private $applicationFactory; /** - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\SampleData\Model\Dependency $sampleDataDependency - * @param \Symfony\Component\Console\Input\ArrayInputFactory $arrayInputFactory - * @param \Composer\Console\ApplicationFactory $applicationFactory + * @var Json + */ + private $serializer; + + /** + * @param Filesystem $filesystem + * @param Dependency $sampleDataDependency + * @param ArrayInputFactory $arrayInputFactory + * @param ApplicationFactory $applicationFactory + * @param Json $serializer */ public function __construct( - \Magento\Framework\Filesystem $filesystem, - \Magento\SampleData\Model\Dependency $sampleDataDependency, - \Symfony\Component\Console\Input\ArrayInputFactory $arrayInputFactory, - \Composer\Console\ApplicationFactory $applicationFactory + Filesystem $filesystem, + Dependency $sampleDataDependency, + ArrayInputFactory $arrayInputFactory, + ApplicationFactory $applicationFactory, + Json $serializer ) { $this->filesystem = $filesystem; $this->sampleDataDependency = $sampleDataDependency; $this->arrayInputFactory = $arrayInputFactory; $this->applicationFactory = $applicationFactory; + $this->serializer = $serializer; parent::__construct(); } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { @@ -79,15 +99,42 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritdoc + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws FileSystemException + * @throws LocalizedException */ protected function execute(InputInterface $input, OutputInterface $output) { - $rootJson = json_decode($this->filesystem->getDirectoryRead(DirectoryList::ROOT)->readFile("composer.json")); - if (!isset($rootJson->version)) { - // @codingStandardsIgnoreLine - $output->writeln('<info>' . 'Git installations must deploy sample data from GitHub; see https://devdocs.magento.com/guides/v2.3/install-gde/install/sample-data-after-clone.html for more information.' . '</info>'); - return; + $rootJson = $this->serializer->unserialize( + $this->filesystem->getDirectoryRead( + DirectoryList::ROOT + )->readFile("composer.json") + ); + if (!isset($rootJson['version'])) { + $magentoProductPackage = array_filter( + $rootJson['require'], + function ($package) { + return false !== strpos($package, 'magento/product-'); + }, + ARRAY_FILTER_USE_KEY + ); + $version = reset($magentoProductPackage); + $output->writeln( + '<info>' . + // @codingStandardsIgnoreLine + 'We don\'t recommend to remove the "version" field from your composer.json; see https://getcomposer.org/doc/02-libraries.md#library-versioning for more information.' . + '</info>' + ); + $restoreVersion = new ArrayInput([ + 'command' => 'config', + 'setting-key' => 'version', + 'setting-value' => [$version], + '--quiet' => 1 + ]); } $this->updateMemoryLimit(); $this->createAuthFile(); @@ -109,6 +156,12 @@ protected function execute(InputInterface $input, OutputInterface $output) /** @var Application $application */ $application = $this->applicationFactory->create(); $application->setAutoExit(false); + if (!empty($restoreVersion)) { + $result = $application->run($restoreVersion, clone $output); + if ($result === 0) { + $output->writeln('<info>The field "version" has been restored.</info>'); + } + } $result = $application->run($commandInput, $output); if ($result !== 0) { $output->writeln( @@ -116,9 +169,15 @@ protected function execute(InputInterface $input, OutputInterface $output) . '</info>' ); $application->resetComposer(); + + return Cli::RETURN_FAILURE; } + + return Cli::RETURN_SUCCESS; } else { $output->writeln('<info>' . 'There is no sample data for current set of modules.' . '</info>'); + + return Cli::RETURN_FAILURE; } } @@ -128,7 +187,7 @@ protected function execute(InputInterface $input, OutputInterface $output) * We create auth.json with correct permissions instead of relying on Composer. * * @return void - * @throws \Exception + * @throws LocalizedException */ private function createAuthFile() { @@ -137,30 +196,51 @@ private function createAuthFile() if (!$directory->isExist(PackagesAuth::PATH_TO_AUTH_FILE)) { try { $directory->writeFile(PackagesAuth::PATH_TO_AUTH_FILE, '{}'); - } catch (\Exception $e) { - $message = 'Error in writing Auth file ' - . $directory->getAbsolutePath(PackagesAuth::PATH_TO_AUTH_FILE) - . '. Please check permissions for writing.'; - throw new \Exception($message); + } catch (Exception $e) { + throw new LocalizedException(__( + 'Error in writing Auth file %1. Please check permissions for writing.', + $directory->getAbsolutePath(PackagesAuth::PATH_TO_AUTH_FILE) + )); } } } /** + * Updates PHP memory limit + * + * @throws InvalidArgumentException * @return void */ private function updateMemoryLimit() { if (function_exists('ini_set')) { - @ini_set('display_errors', 1); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = ini_set('display_errors', 1); + if ($result === false) { + $error = error_get_last(); + throw new InvalidArgumentException(__( + 'Failed to set ini option display_errors to value 1. %1', + $error['message'] + )); + } $memoryLimit = trim(ini_get('memory_limit')); if ($memoryLimit != -1 && $this->getMemoryInBytes($memoryLimit) < 756 * 1024 * 1024) { - @ini_set('memory_limit', '756M'); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = ini_set('memory_limit', '756M'); + if ($result === false) { + $error = error_get_last(); + throw new InvalidArgumentException(__( + 'Failed to set ini option memory_limit to 756M. %1', + $error['message'] + )); + } } } } /** + * Retrieves the memory size in bytes + * * @param string $value * @return int */ 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/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php index 51235dbffc417..3bf664ea6b0d2 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php @@ -20,10 +20,22 @@ use Symfony\Component\Console\Input\ArrayInputFactory; /** + * Common class for tests + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractSampleDataCommandTest extends TestCase { + /* + * Expected arguments for `composer config` to set missing field "version" + */ + private const STUB_EXPECTED_COMPOSER_CONFIG = [ + 'command' => 'config', + 'setting-key' => 'version', + 'setting-value' => ['0.0.1'], + '--quiet' => 1 + ]; + /** * @var ReadInterface|MockObject */ @@ -60,8 +72,10 @@ abstract class AbstractSampleDataCommandTest extends TestCase protected $applicationFactoryMock; /** - * @return void + * @var int */ + private $appRunResult; + protected function setUp(): void { $this->directoryReadMock = $this->getMockForAbstractClass(ReadInterface::class); @@ -74,47 +88,84 @@ protected function setUp(): void } /** - * @param array $sampleDataPackages Array in form [package_name => version_constraint] - * @param string $pathToComposerJson Fake path to composer.json - * @param int $appRunResult Composer exit code + * @param array $sampleDataPackages Array in form [package_name => version_constraint] + * @param string $pathToComposerJson Fake path to composer.json + * @param int $appRunResult Composer exit code + * @param array $composerJsonContent Content of the composer.json * @param array $additionalComposerArgs Additional arguments that composer expects */ protected function setupMocks( $sampleDataPackages, $pathToComposerJson, $appRunResult, + $composerJsonContent = [], $additionalComposerArgs = [] ) { - $this->directoryReadMock->expects($this->any())->method('getAbsolutePath')->willReturn($pathToComposerJson); - $this->directoryReadMock->expects($this->any())->method('readFile')->with('composer.json')->willReturn( - '{"version": "0.0.1"}' - ); - $this->filesystemMock->expects($this->any())->method('getDirectoryRead')->with(DirectoryList::ROOT)->willReturn( - $this->directoryReadMock - ); - $this->sampleDataDependencyMock->expects($this->any())->method('getSampleDataPackages')->willReturn( - $sampleDataPackages - ); + $this->appRunResult = $appRunResult; + $this->directoryReadMock->expects($this->any()) + ->method('getAbsolutePath') + ->willReturn($pathToComposerJson); + $this->directoryReadMock->expects($this->any()) + ->method('readFile') + ->with('composer.json') + ->willReturn(json_encode($composerJsonContent)); + $this->filesystemMock->expects($this->any()) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($this->directoryReadMock); + $this->sampleDataDependencyMock->expects($this->any()) + ->method('getSampleDataPackages') + ->willReturn($sampleDataPackages); $this->arrayInputFactoryMock->expects($this->never())->method('create'); - $this->applicationMock->expects($this->any()) - ->method('run') - ->with( - new ArrayInput( - array_merge( - $this->expectedComposerArguments( - $sampleDataPackages, - $pathToComposerJson + if (!array_key_exists('version', $composerJsonContent)) { + $this->applicationMock->expects($this->any()) + ->method('run') + ->withConsecutive( + [ + 'input' => new ArrayInput( + self::STUB_EXPECTED_COMPOSER_CONFIG ), - $additionalComposerArgs - ) - ), - $this->anything() - ) - ->willReturn($appRunResult); + 'output' => $this->anything() + ], + [ + 'input' => new ArrayInput( + array_merge( + $this->expectedComposerArgumentsSampleDataCommands( + $sampleDataPackages, + $pathToComposerJson + ), + $additionalComposerArgs + ) + ), + 'output' => $this->anything() + ] + )->willReturnOnConsecutiveCalls( + $this->returnValue(0), + $this->returnValue($appRunResult) + ); + } else { + $this->applicationMock->expects($this->any()) + ->method('run') + ->with( + new ArrayInput( + array_merge( + $this->expectedComposerArgumentsSampleDataCommands( + $sampleDataPackages, + $pathToComposerJson + ), + $additionalComposerArgs + ) + ), + $this->anything() + ) + ->willReturn($appRunResult); + } if (($appRunResult !== 0) && !empty($sampleDataPackages)) { - $this->applicationMock->expects($this->once())->method('resetComposer')->willReturnSelf(); + $this->applicationMock->expects($this->any()) + ->method('resetComposer') + ->willReturnSelf(); } $this->applicationFactoryMock->expects($this->any()) @@ -123,14 +174,14 @@ protected function setupMocks( } /** - * Expected arguments for composer based on sample data packages and composer.json path + * Expected arguments for composer based on sample data command * * @param array $sampleDataPackages * @param string $pathToComposerJson * @return array */ - abstract protected function expectedComposerArguments( + abstract protected function expectedComposerArgumentsSampleDataCommands( array $sampleDataPackages, string $pathToComposerJson - ) : array; + ): array; } diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php index 45db83403b4f5..a1186d6015871 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php @@ -7,26 +7,54 @@ namespace Magento\SampleData\Test\Unit\Console\Command; +use Exception; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Serialize\Serializer\Json; use Magento\SampleData\Console\Command\SampleDataDeployCommand; use Magento\Setup\Model\PackagesAuth; +use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Console\Tester\CommandTester; class SampleDataDeployCommandTest extends AbstractSampleDataCommandTest { /** + * @var Json|MockObject + */ + private $serializerMock; + + protected function setUp(): void + { + parent::setUp(); + $this->serializerMock = $this->createMock(Json::class); + } + + /** + * Sets mock for unserialization composer content + * @param array $composerJsonContent + * @return void + */ + protected function setupMockForSerializer(array $composerJsonContent): void + { + $this->serializerMock->expects($this->any()) + ->method('unserialize') + ->will($this->returnValue($composerJsonContent)); + } + + /** + * Sets mocks for auth file + * * @param bool $authExist True to test with existing auth.json, false without + * @return void */ - protected function setupMocksForAuthFile($authExist) + protected function setupMocksForAuthFile(bool $authExist): void { $this->directoryWriteMock->expects($this->once()) ->method('isExist') ->with(PackagesAuth::PATH_TO_AUTH_FILE) ->willReturn($authExist); - $this->directoryWriteMock->expects($authExist ? $this->never() : $this->once())->method('writeFile')->with( - PackagesAuth::PATH_TO_AUTH_FILE, - '{}' - ); + $this->directoryWriteMock->expects($authExist ? $this->never() : $this->once()) + ->method('writeFile') + ->with(PackagesAuth::PATH_TO_AUTH_FILE, '{}'); $this->filesystemMock->expects($this->once()) ->method('getDirectoryWrite') ->with(DirectoryList::COMPOSER_HOME) @@ -34,18 +62,30 @@ protected function setupMocksForAuthFile($authExist) } /** - * @param array $sampleDataPackages - * @param int $appRunResult - int 0 if everything went fine, or an error code - * @param string $expectedMsg - * @param bool $authExist - * @return void + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param array $composerJsonContent + * @param string $expectedMsg + * @param bool $authExist + * @return void * * @dataProvider processDataProvider */ - public function testExecute(array $sampleDataPackages, $appRunResult, $expectedMsg, $authExist) - { - $this->setupMocks($sampleDataPackages, '/path/to/composer.json', $appRunResult); + public function testExecute( + array $sampleDataPackages, + int $appRunResult, + array $composerJsonContent, + string $expectedMsg, + bool $authExist + ): void { + $this->setupMocks( + $sampleDataPackages, + '/path/to/composer.json', + $appRunResult, + $composerJsonContent + ); $this->setupMocksForAuthFile($authExist); + $this->setupMockForSerializer($composerJsonContent); $commandTester = $this->createCommandTester(); $commandTester->execute([]); @@ -53,23 +93,31 @@ public function testExecute(array $sampleDataPackages, $appRunResult, $expectedM } /** - * @param array $sampleDataPackages - * @param int $appRunResult - int 0 if everything went fine, or an error code - * @param string $expectedMsg - * @param bool $authExist - * @return void + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param array $composerJsonContent + * @param string $expectedMsg + * @param bool $authExist + * @return void * * @dataProvider processDataProvider */ - public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult, $expectedMsg, $authExist) - { + public function testExecuteWithNoUpdate( + array $sampleDataPackages, + int $appRunResult, + array $composerJsonContent, + string $expectedMsg, + bool $authExist + ): void { $this->setupMocks( $sampleDataPackages, '/path/to/composer.json', $appRunResult, + $composerJsonContent, ['--no-update' => 1] ); $this->setupMocksForAuthFile($authExist); + $this->setupMockForSerializer($composerJsonContent); $commandInput = ['--no-update' => 1]; $commandTester = $this->createCommandTester(); @@ -79,14 +127,20 @@ public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult } /** + * Data provider + * * @return array */ - public function processDataProvider() + public function processDataProvider(): array { return [ 'No sample data found' => [ 'sampleDataPackages' => [], 'appRunResult' => 1, + 'composerJsonContent' => [ + 'require' => ["magento/product-community-edition" => "0.0.1"], + 'version' => '0.0.1' + ], 'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL, 'authExist' => true, ], @@ -95,15 +149,36 @@ public function processDataProvider() 'magento/module-cms-sample-data' => '1.0.0-beta', ], 'appRunResult' => 1, + 'composerJsonContent' => [ + 'require' => ["magento/product-community-edition" => "0.0.1"], + 'version' => '0.0.1' + ], 'expectedMsg' => 'There is an error during sample data deployment. Composer file will be reverted.' . PHP_EOL, 'authExist' => false, ], + 'Successful sample data installation without field "version"' => [ + 'sampleDataPackages' => [ + 'magento/module-cms-sample-data' => '1.0.0-beta', + ], + 'appRunResult' => 0, + 'composerJsonContent' => [ + 'require' => ["magento/product-community-edition" => "0.0.1"] + ], + // @codingStandardsIgnoreLine + 'expectedMsg' => 'We don\'t recommend to remove the "version" field from your composer.json; see https://getcomposer.org/doc/02-libraries.md#library-versioning for more information.' + . PHP_EOL . 'The field "version" has been restored.' . PHP_EOL, + 'authExist' => true, + ], 'Successful sample data installation' => [ 'sampleDataPackages' => [ 'magento/module-cms-sample-data' => '1.0.0-beta', ], 'appRunResult' => 0, + 'composerJsonContent' => [ + 'require' => ["magento/product-community-edition" => "0.0.1"], + 'version' => '0.0.1' + ], 'expectedMsg' => '', 'authExist' => true, ], @@ -113,7 +188,7 @@ public function processDataProvider() /** * @return void */ - public function testExecuteWithException() + public function testExecuteWithException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage( @@ -122,12 +197,17 @@ public function testExecuteWithException() $this->directoryReadMock->expects($this->once()) ->method('readFile') ->with('composer.json') - ->willReturn('{"version": "0.0.1"}'); + ->willReturn('{"require": {"magento/product-community-edition": "0.0.1"}, "version": "0.0.1"}'); + $this->serializerMock->expects($this->any()) + ->method('unserialize') + ->will($this->returnValue([ + 'require' => ["magento/product-community-edition" => "0.0.1"], + 'version' => '0.0.1' + ])); $this->filesystemMock->expects($this->once()) ->method('getDirectoryRead') ->with(DirectoryList::ROOT) ->willReturn($this->directoryReadMock); - $this->directoryWriteMock->expects($this->once()) ->method('isExist') ->with(PackagesAuth::PATH_TO_AUTH_FILE) @@ -135,7 +215,7 @@ public function testExecuteWithException() $this->directoryWriteMock->expects($this->once()) ->method('writeFile') ->with(PackagesAuth::PATH_TO_AUTH_FILE, '{}') - ->willThrowException(new \Exception('Something went wrong...')); + ->willThrowException(new Exception('Something went wrong...')); $this->directoryWriteMock->expects($this->once()) ->method('getAbsolutePath') ->with(PackagesAuth::PATH_TO_AUTH_FILE) @@ -153,15 +233,15 @@ public function testExecuteWithException() */ private function createCommandTester(): CommandTester { - $commandTester = new CommandTester( + return new CommandTester( new SampleDataDeployCommand( $this->filesystemMock, $this->sampleDataDependencyMock, $this->arrayInputFactoryMock, - $this->applicationFactoryMock + $this->applicationFactoryMock, + $this->serializerMock ) ); - return $commandTester; } /** @@ -169,10 +249,10 @@ private function createCommandTester(): CommandTester * @param $pathToComposerJson * @return array */ - protected function expectedComposerArguments( + protected function expectedComposerArgumentsSampleDataCommands( array $sampleDataPackages, string $pathToComposerJson - ) : array { + ): array { return [ 'command' => 'require', '--working-dir' => $pathToComposerJson, diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php index cbb562ff10f25..9883100ce5c49 100644 --- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php +++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php @@ -10,20 +10,32 @@ use Magento\SampleData\Console\Command\SampleDataRemoveCommand; use Symfony\Component\Console\Tester\CommandTester; +/** + * Tests for command `sampledata:remove` + */ class SampleDataRemoveCommandTest extends AbstractSampleDataCommandTest { - /** - * @param array $sampleDataPackages - * @param int $appRunResult - int 0 if everything went fine, or an error code - * @param string $expectedMsg - * @return void + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param array $composerJsonContent + * @param string $expectedMsg + * @return void * * @dataProvider processDataProvider */ - public function testExecute(array $sampleDataPackages, $appRunResult, $expectedMsg) - { - $this->setupMocks($sampleDataPackages, '/path/to/composer.json', $appRunResult); + public function testExecute( + array $sampleDataPackages, + int $appRunResult, + array $composerJsonContent, + string $expectedMsg + ): void { + $this->setupMocks( + $sampleDataPackages, + '/path/to/composer.json', + $appRunResult, + $composerJsonContent + ); $commandTester = $this->createCommandTester(); $commandTester->execute([]); @@ -31,19 +43,25 @@ public function testExecute(array $sampleDataPackages, $appRunResult, $expectedM } /** - * @param array $sampleDataPackages - * @param int $appRunResult - int 0 if everything went fine, or an error code - * @param string $expectedMsg - * @return void + * @param array $sampleDataPackages + * @param int $appRunResult - int 0 if everything went fine, or an error code + * @param array $composerJsonContent + * @param string $expectedMsg + * @return void * * @dataProvider processDataProvider */ - public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult, $expectedMsg) - { + public function testExecuteWithNoUpdate( + array $sampleDataPackages, + int $appRunResult, + array $composerJsonContent, + string $expectedMsg + ): void { $this->setupMocks( $sampleDataPackages, '/path/to/composer.json', $appRunResult, + $composerJsonContent, ['--no-update' => 1] ); $commandInput = ['--no-update' => 1]; @@ -55,32 +73,51 @@ public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult } /** + * Data provider + * * @return array */ - public function processDataProvider() + public function processDataProvider(): array { return [ - 'No sample data found' => [ - 'sampleDataPackages' => [], + 'No sample data found in require' => [ + 'sampleDataPackages' => [ + 'magento/module-cms-sample-data' => '1.0.0-beta', + ], 'appRunResult' => 1, - 'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL, + 'composerJsonContent' => [ + "require" => [ + "magento/product-community-edition" => "0.0.1", + ], + "version" => "0.0.1" + ], + 'expectedMsg' => 'There is an error during remove sample data.' . PHP_EOL, ], - 'Successful sample data installation' => [ + 'Successful sample data removing' => [ 'sampleDataPackages' => [ 'magento/module-cms-sample-data' => '1.0.0-beta', ], 'appRunResult' => 0, + 'composerJsonContent' => [ + "require" => [ + "magento/product-community-edition" => "0.0.1", + "magento/module-cms-sample-data" => "1.0.0-beta", + ], + "version" => "0.0.1" + ], 'expectedMsg' => '', ], ]; } /** + * Creates command tester + * * @return CommandTester */ private function createCommandTester(): CommandTester { - $commandTester = new CommandTester( + return new CommandTester( new SampleDataRemoveCommand( $this->filesystemMock, $this->sampleDataDependencyMock, @@ -88,15 +125,16 @@ private function createCommandTester(): CommandTester $this->applicationFactoryMock ) ); - return $commandTester; } /** + * Returns expected arguments for command `composer remove` + * * @param $sampleDataPackages * @param $pathToComposerJson * @return array */ - protected function expectedComposerArguments( + protected function expectedComposerArgumentsSampleDataCommands( array $sampleDataPackages, string $pathToComposerJson ) : array { 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/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml index a75f65dffeca3..83e3479c753e4 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml @@ -18,8 +18,6 @@ <testCaseId value="MC-14382" /> <group value="security"/> <group value="mtf_migrated"/> - <!-- skip due to MQE-1964 --> - <group value="skip"/> </annotations> <before> <!-- Log in to Admin Panel --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml index 3d04f3eed4daf..3fffbcd480761 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml @@ -18,8 +18,6 @@ <testCaseId value="MC-14384" /> <group value="security"/> <group value="mtf_migrated"/> - <!-- skip due to MQE-1964 --> - <group value="skip"/> </annotations> <before> <!-- Log in to Admin Panel --> @@ -41,7 +39,7 @@ <argument name="message" value="The password entered for the current user is invalid. Verify the password and try again." /> <argument name="messageType" value="error" /> </actionGroup> - + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillFieldSecondAttempt"> <argument name="role" value="roleAdministrator" /> <argument name="currentAdminPassword" value="{{_ENV.MAGENTO_ADMIN_PASSWORD}}INVALID" /> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml similarity index 97% rename from app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml rename to app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml index 3fb798521fb45..1421b589d5669 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml @@ -9,7 +9,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminLoginAdminUserWithInvalidExpiration"> + <test name="AdminLoginAdminUserWithInvalidExpirationTest"> <annotations> <features value="Security"/> <stories value="Try to login as a user with an invalid expiration date."/> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml similarity index 97% rename from app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml rename to app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml index 5d12650351bc0..9a9ae8f3872ba 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml @@ -9,7 +9,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminLoginAdminUserWithValidExpiration"> + <test name="AdminLoginAdminUserWithValidExpirationTest"> <annotations> <features value="Security"/> <stories value="Login as a user with a valid expiration date."/> 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/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index ea5659cf909ff..21839e1057125 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -571,15 +571,15 @@ protected function _getSitemapRow($url, $lastmod = null, $changefreq = null, $pr foreach ($images->getCollection() as $image) { $row .= '<image:image>'; $row .= '<image:loc>' . $this->_escaper->escapeUrl($image->getUrl()) . '</image:loc>'; - $row .= '<image:title>' . $this->_escaper->escapeHtml($images->getTitle()) . '</image:title>'; + $row .= '<image:title>' . $this->escapeXmlText($images->getTitle()) . '</image:title>'; if ($image->getCaption()) { - $row .= '<image:caption>' . $this->_escaper->escapeHtml($image->getCaption()) . '</image:caption>'; + $row .= '<image:caption>' . $this->escapeXmlText($image->getCaption()) . '</image:caption>'; } $row .= '</image:image>'; } // Add PageMap image for Google web search $row .= '<PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"><DataObject type="thumbnail">'; - $row .= '<Attribute name="name" value="' . $this->_escaper->escapeHtml($images->getTitle()) . '"/>'; + $row .= '<Attribute name="name" value="' . $this->_escaper->escapeHtmlAttr($images->getTitle()) . '"/>'; $row .= '<Attribute name="src" value="' . $this->_escaper->escapeUrl($images->getThumbnail()) . '"/>'; $row .= '</DataObject></PageMap>'; } @@ -587,6 +587,20 @@ protected function _getSitemapRow($url, $lastmod = null, $changefreq = null, $pr return '<url>' . $row . '</url>'; } + /** + * Escape string for XML context. + * + * @param string $text + * @return string + */ + private function escapeXmlText(string $text): string + { + $doc = new \DOMDocument('1.0', 'UTF-8'); + $fragment = $doc->createDocumentFragment(); + $fragment->appendChild($doc->createTextNode($text)); + return $doc->saveXML($fragment); + } + /** * Get sitemap index row * diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php index d8f182adf7732..bfd2c47164cf6 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php @@ -15,6 +15,8 @@ use Magento\Framework\Filesystem\Directory\Write as DirectoryWrite; use Magento\Framework\Filesystem\File\Write; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Translate\InlineInterface; +use Magento\Framework\ZendEscaper; use Magento\Sitemap\Helper\Data; use Magento\Sitemap\Model\ItemProvider\ConfigReaderInterface; use Magento\Sitemap\Model\ItemProvider\ItemProviderInterface; @@ -551,7 +553,7 @@ protected function getModelMock($mockBeforeSave = false) new DataObject( [ 'url' => $storeBaseMediaUrl . 'i/m/image1.png', - 'caption' => 'caption & > title < "' + 'caption' => 'Copyright © caption ™ & > title < "' ] ), new DataObject( @@ -610,7 +612,8 @@ private function getModelConstructorArgs() $objectManager = new ObjectManager($this); $escaper = $objectManager->getObject(Escaper::class); - + $this->setPrivatePropertyValue($escaper, 'escaper', $objectManager->getObject(ZendEscaper::class)); + $this->setPrivatePropertyValue($escaper, 'translateInline', $this->createMock(InlineInterface::class)); $constructArguments = $objectManager->getConstructArguments( Sitemap::class, [ @@ -796,4 +799,21 @@ public function getDocumentRootFromBaseDirUrlDataProvider(): array ], ]; } + + /** + * @param mixed $object + * @param string $attributeName + * @param string $value + */ + private function setPrivatePropertyValue($object, $attributeName, $value): void + { + $attribute = new \ReflectionProperty($object, $attributeName); + if ($attribute->isPublic()) { + $object->$attributeName = $value; + } else { + $attribute->setAccessible(true); + $attribute->setValue($object, $value); + $attribute->setAccessible(false); + } + } } diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml index f31c390c04ec6..ff8087a52e42f 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml @@ -15,7 +15,7 @@ <image:image> <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> - <image:caption>caption & > title < "</image:caption> + <image:caption>Copyright © caption &trade; & > title < "</image:caption> </image:image> <image:image> <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml index 5118663d06372..93b9e159d4b04 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml @@ -33,7 +33,7 @@ <image:image> <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> - <image:caption>caption & > title < "</image:caption> + <image:caption>Copyright © caption &trade; & > title < "</image:caption> </image:image> <image:image> <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> 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/Swatches/Block/LayeredNavigation/RenderLayered.php b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php index fc13372520945..9ba1083adab74 100644 --- a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php +++ b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php @@ -5,11 +5,17 @@ */ namespace Magento\Swatches\Block\LayeredNavigation; -use Magento\Eav\Model\Entity\Attribute; +use Magento\Catalog\Model\Layer\Filter\AbstractFilter; +use Magento\Catalog\Model\Layer\Filter\Item as FilterItem; use Magento\Catalog\Model\ResourceModel\Layer\Filter\AttributeFactory; -use Magento\Framework\View\Element\Template; +use Magento\Eav\Model\Entity\Attribute; use Magento\Eav\Model\Entity\Attribute\Option; -use Magento\Catalog\Model\Layer\Filter\Item as FilterItem; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Magento\Swatches\Helper\Data; +use Magento\Swatches\Helper\Media; +use Magento\Theme\Block\Html\Pager; /** * Class RenderLayered Render Swatches at Layered Navigation @@ -37,7 +43,7 @@ class RenderLayered extends Template protected $eavAttribute; /** - * @var \Magento\Catalog\Model\Layer\Filter\AbstractFilter + * @var AbstractFilter */ protected $filter; @@ -47,41 +53,52 @@ class RenderLayered extends Template protected $layerAttribute; /** - * @var \Magento\Swatches\Helper\Data + * @var Data */ protected $swatchHelper; /** - * @var \Magento\Swatches\Helper\Media + * @var Media */ protected $mediaHelper; /** - * @param Template\Context $context + * @var Pager + */ + private $htmlPagerBlock; + + /** + * @param Context $context * @param Attribute $eavAttribute * @param AttributeFactory $layerAttribute - * @param \Magento\Swatches\Helper\Data $swatchHelper - * @param \Magento\Swatches\Helper\Media $mediaHelper + * @param Data $swatchHelper + * @param Media $mediaHelper * @param array $data + * @param Pager|null $htmlPagerBlock */ public function __construct( - \Magento\Framework\View\Element\Template\Context $context, + Context $context, Attribute $eavAttribute, AttributeFactory $layerAttribute, - \Magento\Swatches\Helper\Data $swatchHelper, - \Magento\Swatches\Helper\Media $mediaHelper, - array $data = [] + Data $swatchHelper, + Media $mediaHelper, + array $data = [], + ?Pager $htmlPagerBlock = null ) { $this->eavAttribute = $eavAttribute; $this->layerAttribute = $layerAttribute; $this->swatchHelper = $swatchHelper; $this->mediaHelper = $mediaHelper; + $this->htmlPagerBlock = $htmlPagerBlock ?? ObjectManager::getInstance()->get(Pager::class); parent::__construct($context, $data); } /** + * Set filter and attribute objects + * * @param \Magento\Catalog\Model\Layer\Filter\AbstractFilter $filter + * * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ @@ -94,6 +111,8 @@ public function setSwatchFilter(\Magento\Catalog\Model\Layer\Filter\AbstractFilt } /** + * Get attribute swatch data + * * @return array */ public function getSwatchData() @@ -114,30 +133,46 @@ public function getSwatchData() $attributeOptionIds = array_keys($attributeOptions); $swatches = $this->swatchHelper->getSwatchesByOptionsId($attributeOptionIds); - $data = [ + return [ 'attribute_id' => $this->eavAttribute->getId(), 'attribute_code' => $this->eavAttribute->getAttributeCode(), 'attribute_label' => $this->eavAttribute->getStoreLabel(), 'options' => $attributeOptions, 'swatches' => $swatches, ]; - - return $data; } /** + * Build filter option url + * * @param string $attributeCode * @param int $optionId + * * @return string */ public function buildUrl($attributeCode, $optionId) { - $query = [$attributeCode => $optionId]; - return $this->_urlBuilder->getUrl('*/*/*', ['_current' => true, '_use_rewrite' => true, '_query' => $query]); + $query = [ + $attributeCode => $optionId, + // exclude current page from urls + $this->htmlPagerBlock->getPageVarName() => null + ]; + + return $this->_urlBuilder->getUrl( + '*/*/*', + [ + '_current' => true, + '_use_rewrite' => true, + '_query' => $query + ] + ); } /** + * Get view data for option with no results + * * @param Option $swatchOption + * * @return array */ protected function getUnusedOption(Option $swatchOption) @@ -150,8 +185,11 @@ protected function getUnusedOption(Option $swatchOption) } /** + * Get option data if visible + * * @param FilterItem[] $filterItems * @param Option $swatchOption + * * @return array */ protected function getFilterOption(array $filterItems, Option $swatchOption) @@ -166,8 +204,11 @@ protected function getFilterOption(array $filterItems, Option $swatchOption) } /** + * Get view data for option + * * @param FilterItem $filterItem * @param Option $swatchOption + * * @return array */ protected function getOptionViewData(FilterItem $filterItem, Option $swatchOption) @@ -187,15 +228,20 @@ protected function getOptionViewData(FilterItem $filterItem, Option $swatchOptio } /** + * Check if option should be visible + * * @param FilterItem $filterItem + * * @return bool */ protected function isOptionVisible(FilterItem $filterItem) { - return $this->isOptionDisabled($filterItem) && $this->isShowEmptyResults() ? false : true; + return !($this->isOptionDisabled($filterItem) && $this->isShowEmptyResults()); } /** + * Check if attribute values should be visible with no results + * * @return bool */ protected function isShowEmptyResults() @@ -204,7 +250,10 @@ protected function isShowEmptyResults() } /** + * Check if option should be disabled + * * @param FilterItem $filterItem + * * @return bool */ protected function isOptionDisabled(FilterItem $filterItem) @@ -213,8 +262,11 @@ protected function isOptionDisabled(FilterItem $filterItem) } /** + * Retrieve filter item by id + * * @param FilterItem[] $filterItems * @param integer $id + * * @return bool|FilterItem */ protected function getFilterItemById(array $filterItems, $id) @@ -228,14 +280,15 @@ protected function getFilterItemById(array $filterItems, $id) } /** + * Get swatch image path + * * @param string $type * @param string $filename + * * @return string */ public function getSwatchPath($type, $filename) { - $imagePath = $this->mediaHelper->getSwatchAttributeImage($type, $filename); - - return $imagePath; + return $this->mediaHelper->getSwatchAttributeImage($type, $filename); } } diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml index 97a391137d8e3..5f3ec07bd4983 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml @@ -19,6 +19,7 @@ <argument name="option2" defaultValue="textSwatchOption2" type="string"/> <argument name="option3" defaultValue="textSwatchOption3" type="string"/> <argument name="usedInProductListing" defaultValue="No" type="string"/> + <argument name="usedInLayeredNavigation" defaultValue="No" type="string"/> </arguments> <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> @@ -41,6 +42,7 @@ <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" stepKey="waitForTabSwitch"/> <selectOption selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" userInput="{{usedInProductListing}}" stepKey="useInProductListing"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="{{usedInLayeredNavigation}}" stepKey="useInLayeredNavigation"/> <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSave"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml new file mode 100644 index 0000000000000..c6266e034bffc --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontRedirectToFirstPageOnFilteringBySwatchTest"> + <annotations> + <features value="Swatches"/> + <stories value="Filter by swatch attribute on plp layered navigation"/> + <title value="Customers are redirected to first plp page after filtering by swatch"/> + <description value="Customers are redirected to first plp page after filtering by swatch"/> + <severity value="MINOR"/> + <group value="Swatches"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct3"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <magentoCLI command="config:set catalog/frontend/grid_per_page 1" stepKey="setOneProductPerPage"/> + <magentoCLI command="config:set catalog/frontend/grid_per_page_values 1" stepKey="setGridPerPage"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addSwatchAttribute"> + <argument name="usedInLayeredNavigation" value="1"/> + </actionGroup> + </before> + + <after> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteSwatchAttribute"> + <argument name="ProductAttribute" value="textSwatchAttribute"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <magentoCLI command="config:set catalog/frontend/grid_per_page 12" stepKey="setDefaultProductsPerPage"/> + <magentoCLI command="config:set catalog/frontend/grid_per_page_values 12,24,36" stepKey="setDefaultGridPerPage"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createSimpleProduct3" stepKey="deleteSimpleProduct3"/> + </after> + + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/{{AddToDefaultSet.attributeSetId}}/" stepKey="onAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="SaveAttributeSet"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndexPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFiltersOnProductIndexPage"/> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct1EditPage"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption1" stepKey="selectProduct1AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct1"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductsGridPage2"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct2EditPage"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption1" stepKey="selectProduct2AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct2"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductsGridPage3"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct3EditPage"> + <argument name="product" value="$$createSimpleProduct3$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption2" stepKey="selectProduct3AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct3"/> + + <magentoCron groups="index" stepKey="runCronIndexer"/> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="StorefrontNavigateCategoryNextPageActionGroup" stepKey="navigateToCategoryNextPage"/> + + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle(textSwatchAttribute.default_label)}}" stepKey="expandAttribute"/> + <click selector="{{StorefrontCategorySidebarSection.attributeNthOption(textSwatchAttribute.attribute_code, '1')}}" stepKey="filterBySwatch1"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <actionGroup ref="AssertStorefrontCategoryCurrentPageIsNthActionGroup" stepKey="assertCurrentPageIsFirst"> + <argument name="expectedPage" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php index 4056bf27f571e..06960c409b476 100644 --- a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php @@ -18,6 +18,7 @@ use Magento\Swatches\Block\LayeredNavigation\RenderLayered; use Magento\Swatches\Helper\Data; use Magento\Swatches\Helper\Media; +use Magento\Theme\Block\Html\Pager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -28,35 +29,60 @@ */ class RenderLayeredTest extends TestCase { - /** @var MockObject */ - protected $contextMock; - - /** @var MockObject */ - protected $requestMock; - - /** @var MockObject */ - protected $urlBuilder; - - /** @var MockObject */ - protected $eavAttributeMock; - - /** @var MockObject */ - protected $layerAttributeFactoryMock; - - /** @var MockObject */ - protected $layerAttributeMock; - - /** @var MockObject */ - protected $swatchHelperMock; - - /** @var MockObject */ - protected $mediaHelperMock; - - /** @var MockObject */ - protected $filterMock; - - /** @var MockObject */ - protected $block; + /** + * @var RenderLayered|MockObject + */ + private $block; + + /** + * @var Context|MockObject + */ + private $contextMock; + + /** + * @var RequestInterface|MockObject + */ + private $requestMock; + + /** + * @var Url|MockObject + */ + private $urlBuilder; + + /** + * @var Attribute|MockObject + */ + private $eavAttributeMock; + + /** + * @var AttributeFactory|MockObject + */ + private $layerAttributeFactoryMock; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Layer\Filter\Attribute|MockObject + */ + private $layerAttributeMock; + + /** + * @var Data|MockObject + */ + private $swatchHelperMock; + + /** + * @var Media|MockObject + */ + private $mediaHelperMock; + + /** + * @var AbstractFilter|MockObject + */ + private $filterMock; + + /** + * @var Pager|MockObject + */ + private $htmlBlockPagerMock; protected function setUp(): void { @@ -66,8 +92,8 @@ protected function setUp(): void Url::class, ['getCurrentUrl', 'getRedirectUrl', 'getUrl'] ); - $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilder); + $this->contextMock->method('getRequest')->willReturn($this->requestMock); + $this->contextMock->method('getUrlBuilder')->willReturn($this->urlBuilder); $this->eavAttributeMock = $this->createMock(Attribute::class); $this->layerAttributeFactoryMock = $this->createPartialMock( AttributeFactory::class, @@ -80,6 +106,7 @@ protected function setUp(): void $this->swatchHelperMock = $this->createMock(Data::class); $this->mediaHelperMock = $this->createMock(Media::class); $this->filterMock = $this->createMock(AbstractFilter::class); + $this->htmlBlockPagerMock = $this->createMock(Pager::class); $this->block = $this->getMockBuilder(RenderLayered::class) ->setMethods(['filter', 'eavAttribute']) @@ -91,6 +118,7 @@ protected function setUp(): void $this->swatchHelperMock, $this->mediaHelperMock, [], + $this->htmlBlockPagerMock ] ) ->getMock(); @@ -114,7 +142,7 @@ public function testGetSwatchData() $item3 = $this->createMock(Item::class); $item4 = $this->createMock(Item::class); - $item1->expects($this->any())->method('__call')->withConsecutive( + $item1->method('__call')->withConsecutive( ['getValue'], ['getCount'], ['getValue'], @@ -128,9 +156,9 @@ public function testGetSwatchData() 'Yellow' ); - $item2->expects($this->any())->method('__call')->with('getValue')->willReturn('blue'); + $item2->method('__call')->with('getValue')->willReturn('blue'); - $item3->expects($this->any())->method('__call')->withConsecutive( + $item3->method('__call')->withConsecutive( ['getValue'], ['getCount'] )->willReturnOnConsecutiveCalls( @@ -138,7 +166,7 @@ public function testGetSwatchData() 0 ); - $item4->expects($this->any())->method('__call')->withConsecutive( + $item4->method('__call')->withConsecutive( ['getValue'], ['getCount'], ['getValue'], @@ -162,22 +190,22 @@ public function testGetSwatchData() $this->block->method('filter')->willReturn($this->filterMock); $option1 = $this->createMock(Option::class); - $option1->expects($this->any())->method('getValue')->willReturn('yellow'); + $option1->method('getValue')->willReturn('yellow'); $option2 = $this->createMock(Option::class); - $option2->expects($this->any())->method('getValue')->willReturn(null); + $option2->method('getValue')->willReturn(null); $option3 = $this->createMock(Option::class); - $option3->expects($this->any())->method('getValue')->willReturn('red'); + $option3->method('getValue')->willReturn('red'); $option4 = $this->createMock(Option::class); - $option4->expects($this->any())->method('getValue')->willReturn('green'); + $option4->method('getValue')->willReturn('green'); $eavAttribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $eavAttribute->expects($this->once()) ->method('getOptions') ->willReturn([$option1, $option2, $option3, $option4]); - $eavAttribute->expects($this->any())->method('getIsFilterable')->willReturn(0); + $eavAttribute->method('getIsFilterable')->willReturn(0); $this->filterMock->expects($this->once())->method('getAttributeModel')->willReturn($eavAttribute); $this->block->method('eavAttribute')->willReturn($eavAttribute); @@ -200,7 +228,7 @@ public function testGetSwatchDataException() { $this->block->method('filter')->willReturn($this->filterMock); $this->block->setSwatchFilter($this->filterMock); - $this->expectException('\RuntimeException'); + $this->expectException(\RuntimeException::class); $this->block->getSwatchData(); } 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/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index f5580461f7d9e..423ac707c6572 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -4,8 +4,8 @@ */ var config = { - 'waitSeconds': 0, - 'map': { + waitSeconds: 0, + map: { '*': { 'ko': 'knockoutjs/knockout', 'knockout': 'knockoutjs/knockout', @@ -13,7 +13,7 @@ var config = { 'rjsResolver': 'mage/requirejs/resolver' } }, - 'shim': { + shim: { 'jquery/jquery-migrate': ['jquery'], 'jquery/jstree/jquery.hotkeys': ['jquery'], 'jquery/hover-intent': ['jquery'], @@ -28,7 +28,7 @@ var config = { }, 'magnifier/magnifier': ['jquery'] }, - 'paths': { + paths: { 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-fp', @@ -40,11 +40,11 @@ var config = { 'tinycolor': 'jquery/spectrum/tinycolor', 'jquery-ui-modules': 'jquery/ui-modules' }, - 'deps': [ + deps: [ 'jquery/jquery-migrate' ], - 'config': { - 'mixins': { + config: { + mixins: { 'jquery/jstree/jquery.jstree': { 'mage/backend/jstree-mixin': true }, @@ -52,7 +52,7 @@ var config = { 'jquery/patches/jquery': true } }, - 'text': { + text: { 'headers': { 'X-Requested-With': 'XMLHttpRequest' } @@ -60,6 +60,27 @@ var config = { } }; +/* eslint-disable max-depth */ +/** + * Adds polyfills only for browser contexts which prevents bundlers from including them. + */ +if (typeof window !== 'undefined' && window.document) { + /** + * Polyfill localStorage and sessionStorage for browsers that do not support them. + */ + try { + if (!window.localStorage || !window.sessionStorage) { + throw new Error(); + } + + localStorage.setItem('storage_test', 1); + localStorage.removeItem('storage_test'); + } catch (e) { + config.deps.push('mage/polyfill'); + } +} +/* eslint-enable max-depth */ + require(['jquery'], function ($) { 'use strict'; diff --git a/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml b/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml index a4a10ef3f6ee9..96f8fbed4c041 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml @@ -10,7 +10,6 @@ <meta name="viewport" content="width=device-width, initial-scale=1"/> <css src="mage/calendar.css"/> <script src="requirejs/require.js"/> - <script src="mage/polyfill.js"/> </head> <body> <referenceBlock name="head.additional"> 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/requirejs-config.js b/app/code/Magento/Ui/view/base/requirejs-config.js index 5e76600673254..4ca2c39781343 100644 --- a/app/code/Magento/Ui/view/base/requirejs-config.js +++ b/app/code/Magento/Ui/view/base/requirejs-config.js @@ -4,6 +4,7 @@ */ var config = { + deps: [], shim: { 'chartjs/Chart.min': ['moment'], 'tiny_mce_4/tinymce.min': { @@ -30,3 +31,29 @@ var config = { } } }; + +/** + * Adds polyfills only for browser contexts which prevents bundlers from including them. + */ +if (typeof window !== 'undefined' && window.document) { + /** + * Polyfill Map and WeakMap for older browsers that do not support them. + */ + if (typeof Map === 'undefined' || typeof WeakMap === 'undefined') { + config.deps.push('es6-collections'); + } + + /** + * Polyfill MutationObserver only for the browsers that do not support it. + */ + if (typeof MutationObserver === 'undefined') { + config.deps.push('MutationObserver'); + } + + /** + * Polyfill FormData object for old browsers that don't have full support for it. + */ + if (typeof FormData === 'undefined' || typeof FormData.prototype.get === 'undefined') { + config.deps.push('FormData'); + } +} diff --git a/app/code/Magento/Ui/view/base/web/js/lib/core/events.js b/app/code/Magento/Ui/view/base/web/js/lib/core/events.js index fdb11cd89f361..15965fba1ad2d 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/core/events.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/core/events.js @@ -6,8 +6,7 @@ /* global WeakMap, Map*/ define([ 'ko', - 'underscore', - 'es6-collections' + 'underscore' ], function (ko, _) { 'use strict'; 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/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js index 6b3c437b90508..0b80a75bf0c18 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js @@ -8,8 +8,7 @@ define([ 'ko', 'underscore', 'mage/utils/wrapper', - 'uiEvents', - 'es6-collections' + 'uiEvents' ], function (ko, _, wrapper, Events) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js b/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js index 826e8ec8c33b4..18e05b8daac68 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/registry/registry.js @@ -9,8 +9,7 @@ /* global WeakMap */ define([ 'jquery', - 'underscore', - 'es6-collections' + 'underscore' ], function ($, _) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js index f8e752fb77af2..cb9f5b13de494 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js @@ -5,7 +5,6 @@ define([ 'jquery', 'underscore', - 'MutationObserver', 'domReady!' ], function ($, _) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/raf.js b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/raf.js index 3ec0996543c7d..bc8e3095b5cd2 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/raf.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/raf.js @@ -4,9 +4,7 @@ */ /* global WeakMap */ -define([ - 'es6-collections' -], function () { +define([], function () { 'use strict'; var processMap = new WeakMap(), diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminSearchUrlRewriteByRequestPathActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminSearchUrlRewriteByRequestPathActionGroup.xml new file mode 100644 index 0000000000000..dfdc840e0dc9f --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminSearchUrlRewriteByRequestPathActionGroup.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="AdminSearchUrlRewriteByRequestPathActionGroup" extends="AdminSearchAndSelectUrlRewriteInGridActionGroup"> + <annotations> + <description>EXTENDS: SearchAndSelectUrlRewrite. Removes 'clickOnRowSelectButton' and 'clickOnEditButton'.</description> + </annotations> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + + <remove keyForRemoval="clickOnRowSelectButton"/> + <remove keyForRemoval="clickOnEditButton"/> + <remove keyForRemoval="waitForEditPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..9de6045d70c03 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathInUrlRewriteGrigActionGroup.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="AssertAdminRequestPathInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the requested path is shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + + <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', requestPath)}}" + stepKey="seeValueInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..8aac6ae54582a --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup.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="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the requested path is not shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + + <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', requestPath)}}" + stepKey="valueIsNotShownInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminStoreValueIsSetForUrlRewriteActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminStoreValueIsSetForUrlRewriteActionGroup.xml new file mode 100644 index 0000000000000..dea0b8d19b428 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminStoreValueIsSetForUrlRewriteActionGroup.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="AssertAdminStoreValueIsSetForUrlRewriteActionGroup"> + <annotations> + <description>Verifies that the proper Store Value is used for URL Rewrite.</description> + </annotations> + <arguments> + <argument name="storeValue" type="string"/> + </arguments> + + <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" + userInput="{{storeValue}}" stepKey="seeStoreValueForCategoryId"/> + </actionGroup> +</actionGroups> 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/ActionGroup/AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup.xml new file mode 100644 index 0000000000000..757c15775dd66 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup.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="AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup"> + <annotations> + <description>Assert redirect to proper URL on the Storefront.</description> + </annotations> + <arguments> + <argument name="target_path" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{target_path}}" stepKey="seePropertUrlRewrite"/> + </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/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml index 036d35d9c3258..d890cde5ecf9d 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml @@ -63,26 +63,25 @@ </actionGroup> <!-- Create simple product with categories created in create data --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductsGrid"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="$$createProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowOfCreatedSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> - <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$rootCategory.name$$" stepKey="fillSearchForInitialCategory"/> - <waitForPageLoad stepKey="waitForCategory1"/> - <click selector="{{AdminProductFormSection.selectCategory($$rootCategory.name$$)}}" stepKey="unselectInitialCategory"/> - <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$category.name$$" stepKey="fillSearchCategory"/> - <waitForPageLoad stepKey="waitForCategory2"/> - <click selector="{{AdminProductFormSection.selectCategory($$category.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> - <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSaved"/> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="SetCategoryByNameActionGroup" stepKey="unselectInitialCategory"> + <argument name="categoryName" value="$$rootCategory.name$$"/> + </actionGroup> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="pressDoneButton"/> + <actionGroup ref="SetCategoryByNameActionGroup" stepKey="setNewCategory"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneButton"/> + <actionGroup ref="SaveProductFormNoSuccessCheckActionGroup" stepKey="saveProduct"/> + <!-- Verify customer see success message --> - <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="seeAssertSimpleProductSaveSuccessMessage"> + <argument name="message" value="You saved the product."/> + </actionGroup> <!-- Grab category Id --> <actionGroup ref="OpenCategoryFromCategoryTreeActionGroup" stepKey="grabCategoryId"> @@ -95,8 +94,13 @@ <argument name="redirectType" value="No"/> <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> </actionGroup> - <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" userInput="{{customStoreGroup.name}}" stepKey="seeStoreValueForCategoryId"/> - <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" userInput="{{customStoreEN.name}}" stepKey="seeStoreViewValueForCategoryId"/> + + <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreValueForCategoryId"> + <argument name="storeValue" value="{{customStoreGroup.name}}"/> + </actionGroup> + <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreViewValueForCategoryId"> + <argument name="storeValue" value="{{customStoreEN.name}}"/> + </actionGroup> <!-- Grab product Id --> <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="grabProductId"> @@ -109,7 +113,12 @@ <argument name="redirectType" value="No"/> <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> </actionGroup> - <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" userInput="{{customStore.name}}" stepKey="seeStoreValueForProductId"/> - <see selector="{{AdminUrlRewriteIndexSection.gridCellByColumnRowNumber('1', 'Store View')}}" userInput="{{storeViewData.name}}" stepKey="seeStoreViewValueForProductId"/> + + <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreValueForProductId"> + <argument name="storeValue" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreViewValueForProductId"> + <argument name="storeValue" value="{{storeViewData.name}}"/> + </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/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml index 9b739b157cddc..16916426167b8 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml @@ -51,8 +51,11 @@ </actionGroup> <!--AssertUrlRewriteSuccessOutsideRedirect--> - <amOnPage url="{{StorefrontHomePage.url}}{{customPermanentUrlRewrite.request_path}}" stepKey="amOnStorefrontPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <seeInCurrentUrl url="{{customPermanentUrlRewrite.target_path}}" stepKey="seeAssertUrlRewrite"/> + <actionGroup ref="NavigateToStorefrontForCreatedPageActionGroup" stepKey="navigateToTheStoreFront"> + <argument name="page" value="{{customPermanentUrlRewrite.request_path}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontUrlRewriteSuccessOutsideRedirectActionGroup" stepKey="seeAssertUrlRewrite"> + <argument name="target_path" value="{{customPermanentUrlRewrite.target_path}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTest.xml index cce0cd11e0199..e98d1b3f526c5 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTest.xml @@ -39,47 +39,60 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Steps --> <!-- 1. Log in to Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- 2. 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', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue4"/> - + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueOne"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueTwo"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueThree"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueFour"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> <!-- 3. Edit Category 1 for DEFAULT Store View: --> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchStoreView"> <argument name="Store" value="_defaultStore.name"/> <argument name="CatName" value="$$simpleSubCategory1.name$$"/> </actionGroup> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> - <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckRedirect2"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="$simpleSubCategory1.custom_attributes[url_key]$-new" stepKey="changeURLKey"/> - <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkUrlKeyRedirect"/> - <!-- 4. Save Category 1 --> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaved"/> - + <!-- 4. Change URL key for category and save changes --> + <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeFirstCategoryUrlKey"> + <argument name="value" value="$simpleSubCategory1.custom_attributes[url_key]$new"/> + </actionGroup> <!-- 5. 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"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue4"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue5"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue6"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue7"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteSecondTime"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueOne"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueTwo"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValuethree"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueFour"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueFive"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$new/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValue1Six"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeInListValueSeven"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewTest.xml index 1876b001eb5bc..de44200994873 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewTest.xml @@ -28,9 +28,13 @@ </before> <remove keyForRemoval="switchStoreView"/> <!-- 3. Edit Category 1 for All store view: --> - <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="goToCategoryPage" after="seeValue4"> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="goToCategoryPage" after="seeValueFour"> <argument name="Category" value="$$simpleSubCategory1$$"/> </actionGroup> - <remove keyForRemoval="uncheckRedirect2"/> + <remove keyForRemoval="changeFirstCategoryUrlKey"/> + <!-- 4. Change URL key for category and save changes --> + <actionGroup ref="ChangeSeoUrlKeyActionGroup" stepKey="changeCategoryUrlKey" after="goToCategoryPage"> + <argument name="value" value="$simpleSubCategory1.custom_attributes[url_key]$new"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml index 14f7c9fb7cbe3..bc2005b32bae2 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestAllStoreViewWithConfigurationTurnedOffTest.xml @@ -28,9 +28,13 @@ </before> <remove keyForRemoval="switchStoreView"/> <!-- 3. Edit Category 1 for All store view: --> - <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="goToCategoryPage" after="seeValue4"> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="goToCategoryPage" after="doNotSeeValueFour"> <argument name="Category" value="$$simpleSubCategory1$$"/> </actionGroup> <remove keyForRemoval="uncheckRedirect2"/> + <!-- 4. Change URL key for category and save changes --> + <actionGroup ref="ChangeSeoUrlKeyActionGroup" stepKey="changeCategoryUrlKey" after="goToCategoryPage"> + <argument name="value" value="$simpleSubCategory1.custom_attributes[url_key]$new"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml index 639cd2c57f7d1..885e09f775c36 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml @@ -47,7 +47,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> <!--Flush cache--> <magentoCLI command="cache:flush" stepKey="cleanCache2"/> @@ -55,59 +55,96 @@ <!-- Steps --> <!-- 1. Log in to Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- 2. 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"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue3"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue4"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueOne"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueTwo"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueThree"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueFour"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> <!-- 3. Edit Category 1 for DEFAULT Store View: --> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchStoreView"> <argument name="Store" value="_defaultStore.name"/> <argument name="CatName" value="$$simpleSubCategory1.name$$"/> </actionGroup> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> - <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckRedirect2"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="$simpleSubCategory1.custom_attributes[url_key]$-new" stepKey="changeURLKey"/> - <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkUrlKeyRedirect"/> - <!-- 4. Save Category 1 --> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaved"/> - <!-- 5. 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="inputProductName1"/> - <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', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue2"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue3"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue4"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue5"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue6"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue7"/> - - <amOnPage url="/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName"/> + <!-- 4. Change URL key for category and save changes --> + <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeCategoryUrlKey"> + <argument name="value" value="$simpleSubCategory1.custom_attributes[url_key]$new"/> + </actionGroup> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage2"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName2"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage3"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName3"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage4"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName4"/> + <!-- 5. Open Marketing - SEO & Search - URL Rewrites --> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteOneMoreTime"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeValueInGrid"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueTwoInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueThreeInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueFourInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueFiveInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueSixInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeValueSevenInGrid"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage5"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName5"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage6"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName6"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage7"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName7"/> + <!-- 6. Assert Redirects work and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSecondAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageThirdAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageFourthAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageFifthAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$new/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSixthAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSeventhAttempt"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.custom_attributes[url_key]$new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml index 1d460b9b668a0..99be6028a3908 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml @@ -41,17 +41,26 @@ <!--Flush cache--> <magentoCLI command="cache:flush" stepKey="cleanCache2"/> </after> + <!-- 1. Log in to Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- 2. 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="seeProducturl"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="dontSeeCategoryProducturlKey"/> - <amOnPage url="/$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="navigateToProductPage"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProductName"/> + <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="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="categoryProductUrlIsNotShown"> + <argument name="requestPath" value="$simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + + <!-- 3. Assert the Redirect works and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + <argument name="productRequestPath" value="/$simpleSubCategory1.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/Test/Mftf/Test/AdminUpdateUserTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml index 501e9520c6367..0943b33e8a711 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminUpdateUserTest.xml @@ -18,8 +18,9 @@ <description value="Change full access role for admin user to custom one with restricted permission (Sales)"/> <group value="user"/> <group value="mtf_migrated"/> - <!-- skip due to MQE-1964 --> - <group value="skip"/> + <skip> + <issueId value="MQE-1964"/> + </skip> </annotations> <before> 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/Vault/Model/Ui/Adminhtml/TokensConfigProvider.php b/app/code/Magento/Vault/Model/Ui/Adminhtml/TokensConfigProvider.php index d79d4ff98f0c8..9fadf9ff7d2c9 100644 --- a/app/code/Magento/Vault/Model/Ui/Adminhtml/TokensConfigProvider.php +++ b/app/code/Magento/Vault/Model/Ui/Adminhtml/TokensConfigProvider.php @@ -24,9 +24,10 @@ use Magento\Vault\Model\VaultPaymentInterface; /** - * Class ConfigProvider + * Provide tokens config * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * * @api * @since 100.1.0 @@ -113,6 +114,8 @@ public function __construct( } /** + * Get list of tokens components + * * @param string $vaultPaymentCode * @return TokenUiComponentInterface[] * @since 100.1.0 @@ -184,6 +187,13 @@ public function getTokensComponents($vaultPaymentCode) ->create(), ] ); + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder->setField(PaymentTokenInterface::IS_VISIBLE) + ->setValue(1) + ->create(), + ] + ); $searchCriteria = $this->searchCriteriaBuilder->create(); @@ -195,6 +205,8 @@ public function getTokensComponents($vaultPaymentCode) } /** + * Get component provider + * * @param string $vaultProviderCode * @return TokenUiComponentProviderInterface|null */ @@ -210,6 +222,7 @@ private function getComponentProvider($vaultProviderCode) /** * Get active vault payment by code + * * @param string $vaultPaymentCode * @return VaultPaymentInterface|null */ @@ -222,6 +235,7 @@ private function getVaultPayment($vaultPaymentCode) /** * Returns payment token entity id by order payment id + * * @return int|null */ private function getPaymentTokenEntityId() @@ -237,6 +251,7 @@ private function getPaymentTokenEntityId() * Returns order payment entity id * Using 'getReordered' for Reorder action * Using 'getOrder' for Edit action + * * @return int */ private function getOrderPaymentEntityId() @@ -250,6 +265,7 @@ private function getOrderPaymentEntityId() /** * Get payment data helper instance + * * @return Data * @deprecated 100.1.0 */ @@ -263,6 +279,7 @@ private function getPaymentDataHelper() /** * Returns order repository instance + * * @return OrderRepositoryInterface * @deprecated 100.2.0 */ @@ -278,6 +295,7 @@ private function getOrderRepository() /** * Returns payment token management instance + * * @return PaymentTokenManagementInterface * @deprecated 100.2.0 */ diff --git a/app/code/Magento/Vault/Test/Unit/Model/Ui/Adminhtml/TokensConfigProviderTest.php b/app/code/Magento/Vault/Test/Unit/Model/Ui/Adminhtml/TokensConfigProviderTest.php index 9307129b908fb..e65349185b88f 100644 --- a/app/code/Magento/Vault/Test/Unit/Model/Ui/Adminhtml/TokensConfigProviderTest.php +++ b/app/code/Magento/Vault/Test/Unit/Model/Ui/Adminhtml/TokensConfigProviderTest.php @@ -35,6 +35,8 @@ use PHPUnit\Framework\TestCase; /** + * Test for TokensConfigProvider + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TokensConfigProviderTest extends TestCase @@ -578,12 +580,15 @@ private function getSearchCriteria($customerId, $entityId, $vaultProviderCode) '2015-01-01 00:00:00', 3 ); + + $isVisibleFilter = $this->createExpectedFilter(PaymentTokenInterface::IS_VISIBLE, 1, 4); + $this->filterBuilder->expects(static::once()) ->method('setConditionType') ->with('gt') ->willReturnSelf(); - $this->searchCriteriaBuilder->expects(self::exactly(4)) + $this->searchCriteriaBuilder->expects(self::exactly(5)) ->method('addFilters') ->willReturnMap( [ @@ -591,6 +596,7 @@ private function getSearchCriteria($customerId, $entityId, $vaultProviderCode) [$codeFilter, $this->searchCriteriaBuilder], [$expiresAtFilter, $this->searchCriteriaBuilder], [$isActiveFilter, $this->searchCriteriaBuilder], + [$isVisibleFilter, $this->searchCriteriaBuilder], ] ); diff --git a/app/code/Magento/Widget/Test/Mftf/Test/ProductsListWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/ProductsListWidgetTest.xml index 9a1aafc649845..593b0df3f3a02 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/ProductsListWidgetTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/ProductsListWidgetTest.xml @@ -56,6 +56,7 @@ <click selector="{{CmsNewPagePageActionsSection.saveAndClose}}" stepKey="clickSaveAndClose"/> <waitForPageLoad stepKey="waitForCmsList2"/> <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearGridFilters"/> <!-- Verify CMS page on storefront --> <waitForElementVisible selector="{{CmsPagesPageActionsSection.select(_newDefaultCmsPage.title)}}" stepKey="waitForCMSPageListItem" /> <click selector="{{CmsPagesPageActionsSection.select(_newDefaultCmsPage.title)}}" stepKey="clickSelect" /> diff --git a/app/code/Magento/Wishlist/Model/Wishlist.php b/app/code/Magento/Wishlist/Model/Wishlist.php index 9b7ff5177afae..cb1a7d956570b 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist.php @@ -181,6 +181,7 @@ class Wishlist extends AbstractModel implements IdentityInterface * @param Json|null $serializer * @param StockRegistryInterface|null $stockRegistry * @param ScopeConfigInterface|null $scopeConfig + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -226,6 +227,7 @@ public function __construct( * * @param int $customerId * @param bool $create Create wishlist if don't exists + * * @return $this */ public function loadByCustomerId($customerId, $create = false) @@ -274,6 +276,7 @@ public function generateSharingCode() * Load by sharing code * * @param string $code + * * @return $this */ public function loadByCode($code) @@ -370,6 +373,7 @@ protected function _addCatalogProduct(Product $product, $qty = 1, $forciblySetQt * Retrieve wishlist item collection * * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + * * @throws NoSuchEntityException */ public function getItemCollection() @@ -389,6 +393,7 @@ public function getItemCollection() * Retrieve wishlist item collection * * @param int $itemId + * * @return false|Item */ public function getItem($itemId) @@ -403,7 +408,9 @@ public function getItem($itemId) * Adding item to wishlist * * @param Item $item + * * @return $this + * * @throws Exception */ public function addItem(Item $item) @@ -424,9 +431,12 @@ public function addItem(Item $item) * @param int|Product $product * @param DataObject|array|string|null $buyRequest * @param bool $forciblySetQty + * * @return Item|string + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * * @throws LocalizedException * @throws InvalidArgumentException */ @@ -529,7 +539,9 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false * Set customer id * * @param int $customerId + * * @return $this + * * @throws LocalizedException */ public function setCustomerId($customerId) @@ -541,6 +553,7 @@ public function setCustomerId($customerId) * Retrieve customer id * * @return int + * * @throws LocalizedException */ public function getCustomerId() @@ -552,6 +565,7 @@ public function getCustomerId() * Retrieve data for save * * @return array + * * @throws LocalizedException */ public function getDataForSave() @@ -567,6 +581,7 @@ public function getDataForSave() * Retrieve shared store ids for current website or all stores if $current is false * * @return array + * * @throws NoSuchEntityException */ public function getSharedStoreIds() @@ -590,6 +605,7 @@ public function getSharedStoreIds() * Set shared store ids * * @param array $storeIds + * * @return $this */ public function setSharedStoreIds($storeIds) @@ -602,6 +618,7 @@ public function setSharedStoreIds($storeIds) * Retrieve wishlist store object * * @return \Magento\Store\Model\Store + * * @throws NoSuchEntityException */ public function getStore() @@ -616,6 +633,7 @@ public function getStore() * Set wishlist store * * @param Store $store + * * @return $this */ public function setStore($store) @@ -653,6 +671,7 @@ public function isSalable() * Retrieve if product has stock or config is set for showing out of stock products * * @param int $productId + * * @return bool */ private function isInStock($productId) @@ -671,7 +690,9 @@ private function isInStock($productId) * Check customer is owner this wishlist * * @param int $customerId + * * @return bool + * * @throws LocalizedException */ public function isOwner($customerId) @@ -696,10 +717,13 @@ public function isOwner($customerId) * @param int|Item $itemId * @param DataObject $buyRequest * @param null|array|DataObject $params + * * @return $this + * * @throws LocalizedException * * @see \Magento\Catalog\Helper\Product::addParamsToBuyRequest() + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -748,10 +772,11 @@ public function updateItem($itemId, $buyRequest, $params = null) throw new LocalizedException(__($resultItem)); } + if ($resultItem->getDescription() != $item->getDescription()) { + $resultItem->setDescription($item->getDescription())->save(); + } + if ($resultItem->getId() != $itemId) { - if ($resultItem->getDescription() != $item->getDescription()) { - $resultItem->setDescription($item->getDescription())->save(); - } $item->isDeleted(true); $this->setDataChanges(true); } else { 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/ActionGroup/AssertStorefrontWishListInvalidEmailsMessageActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/AssertStorefrontWishListInvalidEmailsMessageActionGroup.xml new file mode 100644 index 0000000000000..bdb5e702132dc --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/AssertStorefrontWishListInvalidEmailsMessageActionGroup.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="AssertStorefrontWishListInvalidEmailsMessageActionGroup"> + <arguments> + <argument name="message" type="string"/> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontCustomerWishlistShareSection.errorEmailMessage}}" stepKey="successMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerShareWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerShareWishlistActionGroup.xml index 1f7ac9fc85f50..57404f54a64b2 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerShareWishlistActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerShareWishlistActionGroup.xml @@ -8,7 +8,8 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="StorefrontCustomerShareWishlistActionGroup"> + <actionGroup name="StorefrontCustomerShareWishlistActionGroup" deprecated="Use StorefrontShareCustomerWishlistActionGroup"> + <!-- Deprecated due to Hardcoded WishList Data Using. 18-19 lines --> <annotations> <description>Shares the Wish List from the Storefront Wish List page. PLEASE NOTE: The details for sharing are Hardcoded using 'Wishlist'.</description> </annotations> diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontShareCustomerWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontShareCustomerWishlistActionGroup.xml new file mode 100644 index 0000000000000..6cabeeac1242f --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontShareCustomerWishlistActionGroup.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="StorefrontShareCustomerWishlistActionGroup"> + <arguments> + <argument name="email" type="string"/> + <argument name="message" type="string"/> + </arguments> + + <click selector="{{StorefrontCustomerWishlistProductSection.productShareWishList}}" stepKey="clickMyWishListButton"/> + <fillField userInput="{{email}}" selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistEmail}}" stepKey="fillEmailsForShare"/> + <fillField userInput="{{message}}" selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistTextMessage}}" stepKey="fillShareMessage"/> + <click selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistButton}}" stepKey="sendWishlist"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml b/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml index 4a25a8d449dd3..63b864f682455 100755 --- a/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml @@ -18,4 +18,15 @@ <data key="min_email_text_length_limit">1</data> <data key="max_email_text_length_limit">10000</data> </entity> + <entity name="notValidEmails" type="wishlist"> + <data key="id">null</data> + <var key="product" entityType="product" entityKey="id"/> + <var key="customer_email" entityType="customer" entityKey="email"/> + <var key="customer_password" entityType="customer" entityKey="password"/> + <data key="shareInfo_emails">JohnDoe123456789@,JohnDoe987654321example.com,JohnDoe123456abc@@example.com</data> + <data key="shareInfo_message">Sharing message.</data> + <data key="default_email_text_length_limit">255</data> + <data key="min_email_text_length_limit">1</data> + <data key="max_email_text_length_limit">10000</data> + </entity> </entities> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml index 76b99ba56a327..3f16133be96a9 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml @@ -12,5 +12,6 @@ <element name="ProductShareWishlistEmail" type="input" selector="#email_address"/> <element name="ProductShareWishlistTextMessage" type="input" selector="#message"/> <element name="ProductShareWishlistButton" type="button" selector=".action.submit.primary" timeout="30"/> + <element name="errorEmailMessage" type="input" selector="#email_address-error"/> </section> </sections> 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/StorefrontShareWishlistEntityTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml index 329978462c107..c6b6dc6886f96 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml @@ -48,6 +48,12 @@ <argument name="productVar" value="$createProduct$"/> </actionGroup> - <actionGroup ref="StorefrontCustomerShareWishlistActionGroup" stepKey="shareWishlist"/> + <actionGroup ref="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishlist"> + <argument name="email" value="{{Wishlist.shareInfo_emails}}"/> + <argument name="message" value="{{Wishlist.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontCustomerMessagesActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="Your wish list has been shared."/> + </actionGroup> </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 new file mode 100644 index 0000000000000..0438a1b58e771 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml @@ -0,0 +1,53 @@ +<?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="StorefrontShareWishlistWithNotValidEmailAddressTest"> + <annotations> + <features value="Wishlist"/> + <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> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <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="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishList"> + <argument name="email" value="{{notValidEmails.shareInfo_emails}}"/> + <argument name="message" value="{{notValidEmails.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontWishListInvalidEmailsMessageActionGroup" stepKey="assertErrorMessage"> + <argument name="message" value="Please enter valid email addresses, separated by commas. For example, johndoe@domain.com, johnsmith@domain.com."/> + </actionGroup> + + </test> +</tests> 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/app/etc/di.xml b/app/etc/di.xml index 7b91941fe05d6..ba635d0662755 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -426,6 +426,7 @@ <argument name="reader" xsi:type="object">Magento\Framework\ObjectManager\Config\Reader\Dom\Proxy</argument> <argument name="cacheId" xsi:type="string">plugin-list</argument> <argument name="scopePriorityScheme" xsi:type="array"> + <item name="primary" xsi:type="string">primary</item> <item name="first" xsi:type="string">global</item> </argument> </arguments> diff --git a/composer.json b/composer.json index d487ad5975040..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": "dev-3.0.0-RC3", + "magento/magento2-functional-testing-framework": "3.0.0-RC5", "pdepend/pdepend": "~2.7.1", "phpcompatibility/php-compatibility": "^9.3", "phpmd/phpmd": "^2.8.0", @@ -164,6 +164,7 @@ "magento/module-encryption-key": "*", "magento/module-fedex": "*", "magento/module-gift-message": "*", + "magento/module-gift-message-graph-ql": "*", "magento/module-google-adwords": "*", "magento/module-google-analytics": "*", "magento/module-google-optimizer": "*", @@ -214,6 +215,7 @@ "magento/module-mysql-mq": "*", "magento/module-new-relic-reporting": "*", "magento/module-newsletter": "*", + "magento/module-newsletter-graph-ql": "*", "magento/module-offline-payments": "*", "magento/module-offline-shipping": "*", "magento/module-page-cache": "*", diff --git a/composer.lock b/composer.lock index 6a47e7e44ab69..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": "e86af25d9a4a1942c437cca58f9f1efb", + "content-hash": "f3674961f96b48fdd025a6c94610c8eb", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -297,16 +297,6 @@ "dependency", "package" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-05-06T08:28:10+00:00" }, { @@ -1331,12 +1321,6 @@ "BSD-3-Clause" ], "description": "Replace zendframework and zfcampus packages with their Laminas Project equivalents.", - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-05-20T13:45:39+00:00" }, { @@ -2294,12 +2278,6 @@ "laminas", "mail" ], - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-04-21T16:42:19+00:00" }, { @@ -3292,12 +3270,6 @@ "laminas", "zf" ], - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-05-20T16:45:56+00:00" }, { @@ -3537,16 +3509,6 @@ "logging", "psr-3" ], - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", - "type": "tidelift" - } - ], "time": "2020-05-22T07:31:27+00:00" }, { @@ -4339,16 +4301,6 @@ "parser", "validator" ], - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", - "type": "tidelift" - } - ], "time": "2020-04-30T19:05:18+00:00" }, { @@ -4469,43 +4421,29 @@ ], "description": "Symfony Console 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-03-30T11:41:10+00:00" }, { "name": "symfony/css-selector", - "version": "v5.0.8", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "5f8d5271303dad260692ba73dfa21777d38e124e" + "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/5f8d5271303dad260692ba73dfa21777d38e124e", - "reference": "5f8d5271303dad260692ba73dfa21777d38e124e", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/e544e24472d4c97b2d11ade7caacd446727c6bf9", + "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4550,7 +4488,7 @@ "type": "tidelift" } ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/event-dispatcher", @@ -4620,20 +4558,6 @@ ], "description": "Symfony EventDispatcher 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-03-27T16:54:36+00:00" }, { @@ -4696,26 +4620,26 @@ }, { "name": "symfony/filesystem", - "version": "v5.0.8", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91" + "reference": "6e4320f06d5f2cce0d96530162491f4465179157" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7cd0dafc4353a0f62e307df90b48466379c8cc91", - "reference": "7cd0dafc4353a0f62e307df90b48466379c8cc91", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157", + "reference": "6e4320f06d5f2cce0d96530162491f4465179157", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4756,29 +4680,29 @@ "type": "tidelift" } ], - "time": "2020-04-12T14:40:17+00:00" + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/finder", - "version": "v5.0.8", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "600a52c29afc0d1caa74acbec8d3095ca7e9910d" + "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/600a52c29afc0d1caa74acbec8d3095ca7e9910d", - "reference": "600a52c29afc0d1caa74acbec8d3095ca7e9910d", + "url": "https://api.github.com/repos/symfony/finder/zipball/4298870062bfc667cb78d2b379be4bf5dec5f187", + "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4819,7 +4743,7 @@ "type": "tidelift" } ], - "time": "2020-03-27T16:56:45+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4877,20 +4801,6 @@ "polyfill", "portable" ], - "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:14:59+00:00" }, { @@ -4953,20 +4863,6 @@ "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" }, { @@ -5026,20 +4922,6 @@ "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" }, { @@ -5095,20 +4977,6 @@ "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" }, { @@ -5167,20 +5035,6 @@ "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" }, { @@ -5230,20 +5084,6 @@ ], "description": "Symfony Process 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-04-15T15:56:18+00:00" }, { @@ -5720,16 +5560,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.138.7", + "version": "3.140.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "6b9f3fcea4dfa6092c628c790ca6d369a75453b7" + "reference": "7e37960c1103ee211932be51b2282b41c948a5f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6b9f3fcea4dfa6092c628c790ca6d369a75453b7", - "reference": "6b9f3fcea4dfa6092c628c790ca6d369a75453b7", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7e37960c1103ee211932be51b2282b41c948a5f0", + "reference": "7e37960c1103ee211932be51b2282b41c948a5f0", "shasum": "" }, "require": { @@ -5800,7 +5640,7 @@ "s3", "sdk" ], - "time": "2020-05-22T18:11:09+00:00" + "time": "2020-06-05T18:12:25+00:00" }, { "name": "beberlei/assert", @@ -6018,16 +5858,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.4", + "version": "4.1.5", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "55d8d1d882fa0777e47de17b04c29b3c50fe29e7" + "reference": "24f2345329b1059f1208f65581fc632a4a6e5a55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/55d8d1d882fa0777e47de17b04c29b3c50fe29e7", - "reference": "55d8d1d882fa0777e47de17b04c29b3c50fe29e7", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/24f2345329b1059f1208f65581fc632a4a6e5a55", + "reference": "24f2345329b1059f1208f65581fc632a4a6e5a55", "shasum": "" }, "require": { @@ -6105,7 +5945,7 @@ "type": "open_collective" } ], - "time": "2020-03-23T17:07:20+00:00" + "time": "2020-05-24T13:58:47+00:00" }, { "name": "codeception/lib-asserts", @@ -6249,16 +6089,16 @@ }, { "name": "codeception/module-webdriver", - "version": "1.0.8", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "da55466876d9e73c09917f495b923395b1cdf92a" + "reference": "09c167817393090ce3dbce96027d94656b1963ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/da55466876d9e73c09917f495b923395b1cdf92a", - "reference": "da55466876d9e73c09917f495b923395b1cdf92a", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/09c167817393090ce3dbce96027d94656b1963ce", + "reference": "09c167817393090ce3dbce96027d94656b1963ce", "shasum": "" }, "require": { @@ -6300,7 +6140,7 @@ "browser-testing", "codeception" ], - "time": "2020-04-29T13:45:52+00:00" + "time": "2020-05-31T08:47:24+00:00" }, { "name": "codeception/phpunit-wrapper", @@ -6530,22 +6370,22 @@ }, { "name": "doctrine/annotations", - "version": "1.10.2", + "version": "1.10.3", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "b9d758e831c70751155c698c2f7df4665314a1cb" + "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/b9d758e831c70751155c698c2f7df4665314a1cb", - "reference": "b9d758e831c70751155c698c2f7df4665314a1cb", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d", + "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d", "shasum": "" }, "require": { "doctrine/lexer": "1.*", "ext-tokenizer": "*", - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/cache": "1.*", @@ -6595,24 +6435,24 @@ "docblock", "parser" ], - "time": "2020-04-20T09:18:32+00:00" + "time": "2020-05-25T17:24:27+00:00" }, { "name": "doctrine/cache", - "version": "1.10.0", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62" + "reference": "35a4a70cd94e09e2259dfae7488afc6b474ecbd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/382e7f4db9a12dc6c19431743a2b096041bcdd62", - "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62", + "url": "https://api.github.com/repos/doctrine/cache/zipball/35a4a70cd94e09e2259dfae7488afc6b474ecbd3", + "reference": "35a4a70cd94e09e2259dfae7488afc6b474ecbd3", "shasum": "" }, "require": { - "php": "~7.1" + "php": "~7.1 || ^8.0" }, "conflict": { "doctrine/common": ">2.2,<2.4" @@ -6677,7 +6517,21 @@ "redis", "xcache" ], - "time": "2019-11-29T15:36:20+00:00" + "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" }, { "name": "doctrine/inflector", @@ -6748,20 +6602,20 @@ }, { "name": "doctrine/instantiator", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^6.0", @@ -6800,24 +6654,38 @@ "constructor", "instantiate" ], - "time": "2019-10-21T16:45:58+00:00" + "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" }, { "name": "doctrine/lexer", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", "shasum": "" }, "require": { - "php": "^7.2" + "php": "^7.2 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^6.0", @@ -6862,7 +6730,21 @@ "parser", "php" ], - "time": "2019-10-30T14:39:59+00:00" + "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" }, { "name": "friendsofphp/php-cs-fixer", @@ -6953,12 +6835,6 @@ } ], "description": "A tool to automatically fix PHP code style", - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], "time": "2020-04-15T18:51:10+00:00" }, { @@ -7217,12 +7093,6 @@ "sftp", "storage" ], - "funding": [ - { - "url": "https://offset.earth/frankdejonge", - "type": "other" - } - ], "time": "2020-05-18T15:13:39+00:00" }, { @@ -7333,16 +7203,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "dev-3.0.0-RC3", + "version": "3.0.0-RC5", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "aea30ae1df2fe6618478ba8813864c204561fde3" + "reference": "e5126f4eb476e227e3b668b622159c917f123175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/aea30ae1df2fe6618478ba8813864c204561fde3", - "reference": "aea30ae1df2fe6618478ba8813864c204561fde3", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/e5126f4eb476e227e3b668b622159c917f123175", + "reference": "e5126f4eb476e227e3b668b622159c917f123175", "shasum": "" }, "require": { @@ -7369,7 +7239,8 @@ "symfony/finder": "^5.0", "symfony/mime": "^5.0", "symfony/process": "^4.4", - "vlucas/phpdotenv": "^2.4" + "vlucas/phpdotenv": "^2.4", + "weew/helpers-array": "^1.3" }, "replace": { "facebook/webdriver": "^1.7.1" @@ -7417,7 +7288,7 @@ "magento", "testing" ], - "time": "2020-05-22T19:17:05+00:00" + "time": "2020-06-15T19:51:46+00:00" }, { "name": "mikey179/vfsstream", @@ -8425,20 +8296,6 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" - } - ], "time": "2020-05-05T12:55:44+00:00" }, { @@ -8553,12 +8410,6 @@ "filesystem", "iterator" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-18T05:02:12+00:00" }, { @@ -8707,12 +8558,6 @@ "keywords": [ "timer" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-20T06:00:37+00:00" }, { @@ -8762,12 +8607,6 @@ "keywords": [ "tokenizer" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-05-06T09:56:31+00:00" }, { @@ -8856,16 +8695,6 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-05-22T13:54:05+00:00" }, { @@ -9006,12 +8835,6 @@ ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-30T05:58:10+00:00" }, { @@ -9177,12 +9000,6 @@ "unidiff", "unified diff" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-05-08T05:01:12+00:00" }, { @@ -9236,12 +9053,6 @@ "environment", "hhvm" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-14T13:36:52+00:00" }, { @@ -9925,20 +9736,6 @@ ], "description": "Symfony Config 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-04-15T15:59:10+00:00" }, { @@ -10012,6 +9809,52 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", + "time": "2020-04-28T17:58:55+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337", + "reference": "dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", "funding": [ { "url": "https://symfony.com/sponsor", @@ -10026,35 +9869,41 @@ "type": "tidelift" } ], - "time": "2020-04-28T17:58:55+00:00" + "time": "2020-05-27T08:34:37+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.0.8", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e47fdf8b24edc12022ba52923150ec6484d7f57d" + "reference": "e0d853bddc2b2cfb0d67b0b4496c03fffe1d37fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e47fdf8b24edc12022ba52923150ec6484d7f57d", - "reference": "e47fdf8b24edc12022ba52923150ec6484d7f57d", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e0d853bddc2b2cfb0d67b0b4496c03fffe1d37fa", + "reference": "e0d853bddc2b2cfb0d67b0b4496c03fffe1d37fa", "shasum": "" }, "require": { - "php": "^7.2.5", - "symfony/mime": "^4.4|^5.0", - "symfony/polyfill-mbstring": "~1.1" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.15" }, "require-dev": { "predis/predis": "~1.0", - "symfony/expression-language": "^4.4|^5.0" + "symfony/cache": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -10095,26 +9944,27 @@ "type": "tidelift" } ], - "time": "2020-04-18T20:50:06+00:00" + "time": "2020-05-24T12:18:07+00:00" }, { "name": "symfony/mime", - "version": "v5.0.8", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "5d6c81c39225a750f3f43bee15f03093fb9aaa0b" + "reference": "56261f89385f9d13cf843a5101ac72131190bc91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/5d6c81c39225a750f3f43bee15f03093fb9aaa0b", - "reference": "5d6c81c39225a750f3f43bee15f03093fb9aaa0b", + "url": "https://api.github.com/repos/symfony/mime/zipball/56261f89385f9d13cf843a5101ac72131190bc91", + "reference": "56261f89385f9d13cf843a5101ac72131190bc91", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.15" }, "conflict": { "symfony/mailer": "<4.4" @@ -10126,7 +9976,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -10171,7 +10021,7 @@ "type": "tidelift" } ], - "time": "2020-04-17T03:29:44+00:00" + "time": "2020-05-25T12:33:44+00:00" }, { "name": "symfony/options-resolver", @@ -10225,20 +10075,6 @@ "configuration", "options" ], - "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-04-06T10:40:56+00:00" }, { @@ -10298,6 +10134,68 @@ "portable", "shim" ], + "time": "2020-05-12T16:47:27+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "5e30b2799bc1ad68f7feb62b60a73743589438dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/5e30b2799bc1ad68f7feb62b60a73743589438dd", + "reference": "5e30b2799bc1ad68f7feb62b60a73743589438dd", + "shasum": "" + }, + "require": { + "php": ">=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.17-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "funding": [ { "url": "https://symfony.com/sponsor", @@ -10362,38 +10260,25 @@ ], "description": "Symfony Stopwatch 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-03-27T16:56:45+00:00" }, { "name": "symfony/yaml", - "version": "v5.0.8", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "482fb4e710e5af3e0e78015f19aa716ad953392f" + "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/482fb4e710e5af3e0e78015f19aa716ad953392f", - "reference": "482fb4e710e5af3e0e78015f19aa716ad953392f", + "url": "https://api.github.com/repos/symfony/yaml/zipball/ea342353a3ef4f453809acc4ebc55382231d4d23", + "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -10405,10 +10290,13 @@ "suggest": { "symfony/console": "For validating YAML files using the lint command" }, + "bin": [ + "Resources/bin/yaml-lint" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -10449,7 +10337,7 @@ "type": "tidelift" } ], - "time": "2020-04-28T17:58:55+00:00" + "time": "2020-05-20T17:43:50+00:00" }, { "name": "thecodingmachine/safe", @@ -10724,16 +10612,6 @@ "env", "environment" ], - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", - "type": "tidelift" - } - ], "time": "2020-05-02T13:38:00+00:00" }, { @@ -10826,7 +10704,7 @@ "minimum-stability": "stable", "stability-flags": { "magento/composer": 20, - "magento/magento2-functional-testing-framework": 20 + "magento/magento2-functional-testing-framework": 5 }, "prefer-stable": true, "prefer-lowest": false, diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php new file mode 100644 index 0000000000000..57a607feedb0c --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php @@ -0,0 +1,79 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Model; + +/** + * Class represent simple container to save data + */ +class FixtureCallStorage +{ + /** @var array */ + private $storage = []; + + /** + * Add fixture to storage + * + * @param string $fixture + * @return void + */ + public function addFixtureToStorage(string $fixture): void + { + $this->storage[] = $fixture; + } + + /** + * Get fixture position in storage + * + * @param string $fixture + * @return null|int + */ + public function getFixturePosition(string $fixture): ?int + { + return array_search($fixture, $this->storage) ?: null; + } + + /** + * Get storage + * + * @return array + */ + public function getStorage(): array + { + return $this->storage; + } + + /** + * Get fixtures count in storage + * + * @param string $fixture + * @return int + */ + public function getFixturesCount(string $fixture = ''): int + { + $count = count($this->storage); + if ($fixture) { + $result = array_filter($this->storage, function ($storedFixture) use ($fixture) { + return $storedFixture === $fixture; + }); + $count = count($result); + } + + return $count; + } + + /** + * Clear storage + * + * @return void + */ + public function clearStorage(): void + { + $this->storage = []; + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/composer.json b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/composer.json new file mode 100644 index 0000000000000..47ac2d4ac4a3b --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-override-config-test", + "description": "module for override config check", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleOverrideConfig" + ] + ] + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..8c0badac4b1d1 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="test_section" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="test_group" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> + <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"/> + </group> + </section> + </system> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/config.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/config.xml new file mode 100644 index 0000000000000..3b2f2a1ddde1e --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/config.xml @@ -0,0 +1,27 @@ +<?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> + <test_section> + <test_group> + <field_1>1st field default value</field_1> + <field_2>2nd field default value</field_2> + <field_3>3rd field default value</field_3> + </test_group> + </test_section> + </default> + <websites> + <base> + <test_section> + <test_group> + <field_3>3rd field website scope default value</field_3> + </test_group> + </test_section> + </base> + </websites> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/module.xml new file mode 100644 index 0000000000000..f9d63847959df --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/etc/module.xml @@ -0,0 +1,10 @@ +<?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:Module/etc/module.xsd"> + <module name="Magento_TestModuleOverrideConfig" /> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/registration.php new file mode 100644 index 0000000000000..16ffc73cef00f --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig', __DIR__); +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/Test/Api/_files/overrides.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/Test/Api/_files/overrides.xml new file mode 100644 index 0000000000000..bda41e51aa5c8 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/Test/Api/_files/overrides.xml @@ -0,0 +1,150 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<overrides> + <!-- test node determine to which class config inside node should be applied --> + <test class="Magento\TestModuleOverrideConfig\MagentoApiConfigFixture\AddFixtureTest"> + <!-- Node bellow will add magentoConfigFixture to fixtures list + 'scopeType' required attribute and accept such values: store|website + 'scopeCode' store|website code + skip 'scopeType' and 'scopeCode' attributes to set value in default scope + 'path' required attribute determine config path, 'value' attribute determine which value will be set for provided path + to add fixture to fixtures list + --> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden value for full class"/> + <!-- method node determine to which test method config inside node should be applied --> + <method name="testAddFixtureToMethod"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden value for method"/> + <!-- dataSet node determine for which data set config inside should be applied --> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden value for data set"/> + </dataSet> + </method> + <method name="testAddFixtureOnWebsiteScope"> + <magentoConfigFixture scopeType="website" scopeCode="base" path="test_section/test_group/field_1" value="overridden value for method on website scope"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiConfigFixture\RemoveFixtureTest"> + <!-- 'remove' attribute accept bool values, if value set to 'true' this node will remove matching fixture from fixtures list--> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" remove="true"/> + <method name="testRemoveFixtureForMethod"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_2" remove="true"/> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_3" remove="true"/> + </dataSet> + </method> + <method name="testRemoveWebsiteScopeFixture"> + <magentoConfigFixture scopeType="website" scopeCode="base" path="test_section/test_group/field_3" remove="true"/> + </method> + <method name="testRemoveWebsiteScopeFixtureWithScopeCode"> + <magentoConfigFixture scopeType="website" scopeCode="base" path="test_section/test_group/field_3" remove="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiConfigFixture\ReplaceFixtureTest"> + <!-- Node bellow will replace value for matching fixture + 'newValue' attribute determine to which value current value in matching fixture should be replaced --> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for class"/> + <method name="testReplaceFixtureForMethod"> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for data set"/> + </dataSet> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for method"/> + </method> + <method name="testReplaceFixtureViaThirdModule" > + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for data set from second module"/> + </dataSet> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for method from second module"/> + </method> + <method name="testReplaceWebsiteScopedFixture"> + <magentoConfigFixture scopeType="website" scopeCode="base" path="test_section/test_group/field_1" newValue="Overridden value for website scope"/> + </method> + <method name="testReplaceDefaultConfig"> + <magentoConfigFixture path="test_section/test_group/field_1" newValue="Overridden value for default scope"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\AddFixtureTest"> + <!-- 'path' attribute determine path to fixture for which config should be applied + if only this attribute specified the fixture with such path will be applied --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php"/> + <method name="testAddFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php"/> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php"/> + </dataSet> + </method> + <method name="testAddSameFixtures"> + <!-- Few same data fixtures can be applied for one test --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php"/> + </method> + <method name="testAddFixtureWithRequiredFixture"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture_with_required_fixture.php"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\RemoveFixtureTest"> + <!-- 'remove' attribute support boolean values, to remove fixture with specified path you need to set this 'remove' attribute to 'true' --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" remove="true"/> + <method name="testRemoveFixtureForMethod"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <dataSet name="second_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + </dataSet> + </method> + <method name="testRemoveSameFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\ReplaceFixtureTest"> + <!-- Node bellow will call specified in 'newPath' attribute fixture instead of fixture specified in 'path' attribute + if such fixture exist in fixtures list --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <!-- If you specify data fixture to replace you should also specify rollback fixture in the separate node--> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + + <method name="testReplaceFixturesForMethod"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="second_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> + </dataSet> + </method> + <method name="testReplaceFixtureViaThirdModule"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> + </dataSet> + </method> + <method name="testReplaceRequiredFixture"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\SortFixturesTest"> + <!-- 'after' attribute determine after which fixture current fixture should be placed, '-' value means that fixture shold be placed after all --> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" after="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <method name="testSortFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" after="-"/> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" before="-"/> + </dataSet> + </method> + </test> + <!-- 'skip' attribute accept boolean values and will mark test as skipped test for which it specified if value set to 'true'--> + <test class="Magento\TestModuleOverrideConfig\Skip\SkipClassTest" skip="true"/> + <test class="Magento\TestModuleOverrideConfig\Skip\SkipMethodTest"> + <method name="testMethodSkip" skip="true"/> + </test> + <test class="Magento\TestModuleOverrideConfig\Skip\SkipDataSetTest"> + <method name="testSkipDataSet"> + <dataSet name="first_data_set" skip="true"/> + </method> + </test> +</overrides> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/composer.json b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/composer.json new file mode 100644 index 0000000000000..43b7bec56945d --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-override-config2-test", + "description": "module for override config check", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleOverrideConfig2" + ] + ] + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/etc/module.xml new file mode 100644 index 0000000000000..6432681d22e1d --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/etc/module.xml @@ -0,0 +1,14 @@ +<?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:Module/etc/module.xsd"> + <module name="Magento_TestModuleOverrideConfig2"> + <sequence> + <module name="Magento_TestModuleOverrideConfig"/> + </sequence> + </module> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/registration.php new file mode 100644 index 0000000000000..ac3f1763eb93b --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig2/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig2') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig2', __DIR__); +} 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 new file mode 100644 index 0000000000000..b0b114890041b --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<overrides> + <test class="Magento\TestModuleOverrideConfig\MagentoApiConfigFixture\ReplaceFixtureTest"> + <method name="testReplaceFixtureViaThirdModule"> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for data set from third module"/> + </dataSet> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="Overridden fixture for method from third module"/> + </method> + </test> + + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\AddFixtureTest"> + <method name="testAddSameFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixtureBeforeTransaction\AddFixtureTest"> + <method name="testAddSameFixtures"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\ReplaceFixtureTest"> + <method name="testReplaceFixtureViaThirdModule"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + </dataSet> + </method> + <method name="testReplaceRequiredFixtureViaThirdModule"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" newPath="Magento/TestModuleOverrideConfig3/_files/fixture1_third_module.php" /> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\MagentoApiDataFixture\SortFixturesTest"> + <method name="testSortFixtures"> + <dataSet name="first_data_set"> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig3/_files/fixture1_third_module.php" before="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" /> + </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/_files/Magento/TestModuleOverrideConfig3/composer.json b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/composer.json new file mode 100644 index 0000000000000..432b2ef703a57 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-override-config3-test", + "description": "module for override config check", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleOverrideConfig3" + ] + ] + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/etc/module.xml new file mode 100644 index 0000000000000..ef3693fd036f3 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/etc/module.xml @@ -0,0 +1,14 @@ +<?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:Module/etc/module.xsd"> + <module name="Magento_TestModuleOverrideConfig3"> + <sequence> + <module name="Magento_TestModuleOverrideConfig2"/> + </sequence> + </module> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/registration.php new file mode 100644 index 0000000000000..e3217d8f97e8a --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig3') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleOverrideConfig3', __DIR__); +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php index 8061cb138660d..3ef6e6618c6c5 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php @@ -7,13 +7,14 @@ namespace Magento\TestFramework\Annotation; -use Magento\Config\Model\Config; use Magento\Config\Model\ResourceModel\Config as ConfigResource; -use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\TestFramework\Helper\Bootstrap; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; -use PHPUnit\Framework\TestCase; +use Magento\TestFramework\App\ApiMutableScopeConfig; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestFramework\Helper\Bootstrap; /** * @inheritDoc @@ -21,167 +22,162 @@ class ApiConfigFixture extends ConfigFixture { /** - * Original values for global configuration options that need to be restored + * Values need to be deleted form the database * * @var array */ - private $_globalConfigValues = []; + private $valuesToDeleteFromDatabase = []; /** - * Original values for store-scoped configuration options that need to be restored - * - * @var array + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - private $_storeConfigValues = []; + protected function setStoreConfigValue(array $matches, $configPathAndValue): void + { + $storeCode = $matches[0]; + [$configScope, $configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 3); + /** @var ConfigStorage $configStorage */ + $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); + if (!$configStorage->checkIsRecordExist($configPath, ScopeInterface::SCOPE_STORES, $storeCode)) { + $this->valuesToDeleteFromDatabase[$storeCode][$configPath ?? ''] = $requiredValue ?? ''; + } + + parent::setStoreConfigValue($matches, $configPathAndValue); + } /** - * Values need to be deleted form the database - * - * @var array + * @inheritdoc */ - private $_valuesToDeleteFromDatabase = []; + protected function setGlobalConfigValue($configPathAndValue): void + { + [$configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 2); + /** @var ConfigStorage $configStorage */ + $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); + if (!$configStorage->checkIsRecordExist($configPath)) { + $this->valuesToDeleteFromDatabase['global'][$configPath] = $requiredValue; + } + + $originalValue = $this->getScopeConfigValue($configPath, ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + $this->globalConfigValues[$configPath] = $originalValue; + $this->_setConfigValue($configPath, $requiredValue); + } /** - * Assign required config values and save original ones - * - * @param TestCase $test + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - protected function _assignConfigData(TestCase $test) + protected function setWebsiteConfigValue(array $matches, $configPathAndValue): void { - $annotations = $test->getAnnotations(); - if (!isset($annotations['method'][$this->annotation])) { - return; + $websiteCode = $matches[0]; + [$configScope, $configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 3); + /** @var ConfigStorage $configStorage */ + $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); + if (!$configStorage->checkIsRecordExist($configPath, ScopeInterface::SCOPE_WEBSITES, $websiteCode)) { + $this->valuesToDeleteFromDatabase[$websiteCode][$configPath ?? ''] = $requiredValue ?? ''; } - foreach ($annotations['method'][$this->annotation] as $configPathAndValue) { - if (preg_match('/^.+?(?=_store\s)/', $configPathAndValue, $matches)) { - /* Store-scoped config value */ - $storeCode = $matches[0]; - $parts = preg_split('/\s+/', $configPathAndValue, 3); - list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; - $originalValue = $this->_getConfigValue($configPath, $storeCode); - $this->_storeConfigValues[$storeCode][$configPath] = $originalValue; - if ($this->checkIfValueExist($configPath, $storeCode)) { - $this->_valuesToDeleteFromDatabase[$storeCode][$configPath] = $requiredValue; - } - $this->_setConfigValue($configPath, $requiredValue, $storeCode); - } else { - /* Global config value */ - list($configPath, $requiredValue) = preg_split('/\s+/', $configPathAndValue, 2); - - $originalValue = $this->_getConfigValue($configPath); - $this->_globalConfigValues[$configPath] = $originalValue; - if ($this->checkIfValueExist($configPath)) { - $this->_valuesToDeleteFromDatabase['global'][$configPath] = $requiredValue; - } - $this->_setConfigValue($configPath, $requiredValue); - } - } + parent::setWebsiteConfigValue($matches, $configPathAndValue); } /** - * Restore original values for changed config options + * @inheritDoc + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _restoreConfigData() { + /** @var ConfigResource $configResource */ $configResource = Bootstrap::getObjectManager()->get(ConfigResource::class); - /* Restore global values */ - foreach ($this->_globalConfigValues as $configPath => $originalValue) { - if (isset($this->_valuesToDeleteFromDatabase['global'][$configPath])) { + foreach ($this->globalConfigValues as $configPath => $originalValue) { + if (isset($this->valuesToDeleteFromDatabase['global'][$configPath])) { $configResource->deleteConfig($configPath); } else { $this->_setConfigValue($configPath, $originalValue); } } - $this->_globalConfigValues = []; - + $this->globalConfigValues = []; /* Restore store-scoped values */ - foreach ($this->_storeConfigValues as $storeCode => $originalData) { + foreach ($this->storeConfigValues as $storeCode => $originalData) { foreach ($originalData as $configPath => $originalValue) { - if (empty($storeCode)) { - $storeCode = null; + $storeCode = $storeCode ?: null; + if (isset($this->valuesToDeleteFromDatabase[$storeCode][$configPath])) { + $scopeId = $this->getIdByScopeType(ScopeInterface::SCOPE_STORES, $storeCode); + $configResource->deleteConfig($configPath, ScopeInterface::SCOPE_STORES, $scopeId); + } else { + $this->setScopeConfigValue( + $configPath, + $originalValue, + ScopeInterface::SCOPE_STORES, + $storeCode + ); } - if (isset($this->_valuesToDeleteFromDatabase[$storeCode][$configPath])) { - $scopeId = $this->getStoreIdByCode($storeCode); - $configResource->deleteConfig($configPath, 'stores', $scopeId); + } + } + $this->storeConfigValues = []; + /* Restore website-scoped values */ + foreach ($this->websiteConfigValues as $websiteCode => $originalData) { + foreach ($originalData as $configPath => $originalValue) { + $websiteCode = $websiteCode ?: null; + if (isset($this->valuesToDeleteFromDatabase[$websiteCode][$configPath])) { + $scopeId = $this->getIdByScopeType(ScopeInterface::SCOPE_WEBSITES, $websiteCode); + $configResource->deleteConfig($configPath, ScopeInterface::SCOPE_WEBSITES, $scopeId); } else { - $this->_setConfigValue($configPath, $originalValue, $storeCode); + $this->setScopeConfigValue( + $configPath, + $originalValue, + ScopeInterface::SCOPE_WEBSITES, + $websiteCode + ); } } } - $this->_storeConfigValues = []; + $this->websiteConfigValues = []; } /** - * Load configs by path and scope - * - * @param string $configPath - * @param string $storeCode - * @return Config[] + * @inheritdoc */ - private function loadConfigs(string $configPath, string $storeCode = null): array + protected function getMutableScopeConfig(): MutableScopeConfigInterface { - $configCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); - $collection = $configCollectionFactory->create(); - $scope = $storeCode ? 'stores' : 'default'; - $scopeId = $storeCode ? $this->getStoreIdByCode($storeCode) : 0; - - $collection->addScopeFilter($scope, $scopeId, $configPath); - return $collection->getItems(); + return Bootstrap::getObjectManager() + ->get(ApiMutableScopeConfig::class); } /** - * Check if config exist in the database - * - * @param string $configPath - * @param string|null $storeCode + * @inheritdoc */ - private function checkIfValueExist(string $configPath, string $storeCode = null): bool + protected function getScopeConfigValue(string $configPath, string $scopeType, string $scopeCode = null): ?string { - $configs = $this->loadConfigs($configPath, $storeCode); + /** @var ConfigStorage $configStorage */ + $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); + $result = $configStorage->getValueFromDb($configPath, $scopeType, $scopeCode); - return !(bool)$configs; + return $result ?: null; } /** - * Returns the store ID by the store code + * Get id by code * - * @param string $storeCode + * @param string $scopeType + * @param string|null $scopeId * @return int */ - private function getStoreIdByCode(string $storeCode): int + private function getIdByScopeType(string $scopeType, ?string $scopeId): int { + $id = 0; + /** @var StoreManagerInterface $storeManager */ $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); - $store = $storeManager->getStore($storeCode); - return (int)$store->getId(); - } - - /** - * @inheritDoc - */ - protected function _setConfigValue($configPath, $value, $storeCode = false) - { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - if ($storeCode === false) { - $objectManager->get( - \Magento\TestFramework\App\ApiMutableScopeConfig::class - )->setValue( - $configPath, - $value, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT - ); - - return; + switch ($scopeType) { + case ScopeInterface::SCOPE_WEBSITES: + $id = (int)$storeManager->getWebsite($scopeId)->getId(); + break; + case ScopeInterface::SCOPE_STORES: + $id = (int)$storeManager->getStore($scopeId)->getId(); + break; + default: + break; } - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\TestFramework\App\ApiMutableScopeConfig::class - )->setValue( - $configPath, - $value, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $storeCode - ); + + return $id; } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php index 88ac682f6b282..d99b056ca359e 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php @@ -12,46 +12,27 @@ namespace Magento\TestFramework\Annotation; -class ApiDataFixture -{ - /** - * @var string - */ - protected $_fixtureBaseDir; +use Magento\Customer\Model\Metadata\AttributeMetadataCache; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; - /** - * Fixtures that have been applied - * - * @var array - */ - private $_appliedFixtures = []; - - /** - * Constructor - * - * @param string $fixtureBaseDir - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function __construct($fixtureBaseDir) - { - if (!is_dir($fixtureBaseDir)) { - throw new \Magento\Framework\Exception\LocalizedException( - __("Fixture base directory '%1' does not exist.", $fixtureBaseDir) - ); - } - $this->_fixtureBaseDir = realpath($fixtureBaseDir); - } +/** + * Implementation of the @magentoApiDataFixture DocBlock annotation. + */ +class ApiDataFixture extends DataFixture +{ + public const ANNOTATION = 'magentoApiDataFixture'; /** * Handler for 'startTest' event * - * @param \PHPUnit\Framework\TestCase $test + * @param TestCase $test */ - public function startTest(\PHPUnit\Framework\TestCase $test) + public function startTest(TestCase $test) { - \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + Bootstrap::getInstance()->reinitialize(); /** Apply method level fixtures if thy are available, apply class level fixtures otherwise */ - $this->_applyFixtures($this->_getFixtures('method', $test) ?: $this->_getFixtures('class', $test)); + $this->_applyFixtures($this->_getFixtures($test, 'method') ?: $this->_getFixtures($test, 'class')); } /** @@ -60,109 +41,15 @@ public function startTest(\PHPUnit\Framework\TestCase $test) public function endTest() { $this->_revertFixtures(); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $objectManager->get(\Magento\Customer\Model\Metadata\AttributeMetadataCache::class)->clean(); - } - - /** - * Retrieve fixtures from annotation - * - * @param string $scope 'class' or 'method' - * @param \PHPUnit\Framework\TestCase $test - * @return array - * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function _getFixtures($scope, \PHPUnit\Framework\TestCase $test) - { - $annotations = $test->getAnnotations(); - $result = []; - if (!empty($annotations[$scope]['magentoApiDataFixture'])) { - foreach ($annotations[$scope]['magentoApiDataFixture'] as $fixture) { - if (strpos($fixture, '\\') !== false) { - // usage of a single directory separator symbol streamlines search across the source code - throw new \Magento\Framework\Exception\LocalizedException( - __('Directory separator "\\" is prohibited in fixture declaration.') - ); - } - $fixtureMethod = [get_class($test), $fixture]; - if (is_callable($fixtureMethod)) { - $result[] = $fixtureMethod; - } else { - $result[] = $this->_fixtureBaseDir . '/' . $fixture; - } - } - } - return $result; - } - - /** - * Execute single fixture script - * - * @param string|array $fixture - * @throws \Throwable - */ - protected function _applyOneFixture($fixture) - { - try { - if (is_callable($fixture)) { - call_user_func($fixture); - } else { - require $fixture; - } - } catch (\Exception $e) { - throw new \Exception( - sprintf( - "Exception occurred when running the %s fixture: \n%s", - (\is_array($fixture) || is_scalar($fixture) ? json_encode($fixture) : 'callback'), - $e->getMessage() - ) - ); - } - $this->_appliedFixtures[] = $fixture; - } - - /** - * Execute fixture scripts if any - * - * @param array $fixtures - * @throws \Magento\Framework\Exception\LocalizedException - */ - protected function _applyFixtures(array $fixtures) - { - /* Execute fixture scripts */ - foreach ($fixtures as $oneFixture) { - /* Skip already applied fixtures */ - if (!in_array($oneFixture, $this->_appliedFixtures, true)) { - $this->_applyOneFixture($oneFixture); - } - } + $objectManager = Bootstrap::getObjectManager(); + $objectManager->get(AttributeMetadataCache::class)->clean(); } /** - * Revert changes done by fixtures + * @inheritdoc */ - protected function _revertFixtures() + protected function getAnnotation(): string { - $appliedFixtures = array_reverse($this->_appliedFixtures); - foreach ($appliedFixtures as $fixture) { - if (is_callable($fixture)) { - $fixture[1] .= 'Rollback'; - if (is_callable($fixture)) { - $this->_applyOneFixture($fixture); - } - } else { - $fileInfo = pathinfo($fixture); - $extension = ''; - if (isset($fileInfo['extension'])) { - $extension = '.' . $fileInfo['extension']; - } - $rollbackScript = $fileInfo['dirname'] . '/' . $fileInfo['filename'] . '_rollback' . $extension; - if (file_exists($rollbackScript)) { - $this->_applyOneFixture($rollbackScript); - } - } - } - $this->_appliedFixtures = []; + return self::ANNOTATION; } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/ApiSuiteLoader.php b/dev/tests/api-functional/framework/Magento/TestFramework/ApiSuiteLoader.php new file mode 100644 index 0000000000000..ea8ee3a358410 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/ApiSuiteLoader.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework; + +/** + * Custom suite loader for adding wrapper for tests. + */ +class ApiSuiteLoader extends SuiteLoader +{ + +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php b/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php index fa0cebece9a96..e94ba706e4796 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php @@ -5,31 +5,58 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\TestFramework\App; +use Magento\Config\Model\Config\Factory as ConfigFactory; use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\TestFramework\ObjectManager; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\ScopeInterface; /** * @inheritdoc */ class ApiMutableScopeConfig implements MutableScopeConfigInterface { + /** @var Config */ + private $testAppConfig; + + /** @var StoreRepositoryInterface */ + private $storeRepository; + + /** @var WebsiteRepositoryInterface */ + private $websiteRepository; + + /** @var ConfigFactory */ + private $configFactory; + /** - * @var Config + * @param ScopeConfigInterface $config + * @param StoreRepositoryInterface $storeRepository + * @param WebsiteRepositoryInterface $websiteRepository + * @param ConfigFactory $configFactory */ - private $testAppConfig; + public function __construct( + ScopeConfigInterface $config, + StoreRepositoryInterface $storeRepository, + WebsiteRepositoryInterface $websiteRepository, + ConfigFactory $configFactory + ) { + $this->testAppConfig = $config; + $this->storeRepository = $storeRepository; + $this->websiteRepository = $websiteRepository; + $this->configFactory = $configFactory; + } /** * @inheritdoc */ public function isSetFlag($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null) { - return $this->getTestAppConfig()->isSetFlag($path, $scopeType, $scopeCode); + return $this->testAppConfig->isSetFlag($path, $scopeType, $scopeCode); } /** @@ -37,7 +64,7 @@ public function isSetFlag($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_D */ public function getValue($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null) { - return $this->getTestAppConfig()->getValue($path, $scopeType, $scopeCode); + return $this->testAppConfig->getValue($path, $scopeType, $scopeCode); } /** @@ -46,11 +73,11 @@ public function getValue($path, $scopeType = ScopeConfigInterface::SCOPE_TYPE_DE public function setValue( $path, $value, - $scopeType = \Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null ) { $this->persistConfig($path, $value, $scopeType, $scopeCode); - return $this->getTestAppConfig()->setValue($path, $value, $scopeType, $scopeCode); + return $this->testAppConfig->setValue($path, $value, $scopeType, $scopeCode); } /** @@ -60,21 +87,7 @@ public function setValue( */ public function clean() { - $this->getTestAppConfig()->clean(); - } - - /** - * Retrieve test app config instance - * - * @return \Magento\TestFramework\App\Config - */ - private function getTestAppConfig() - { - if (!$this->testAppConfig) { - $this->testAppConfig = ObjectManager::getInstance()->get(ScopeConfigInterface::class); - } - - return $this->testAppConfig; + $this->testAppConfig->clean(); } /** @@ -84,18 +97,12 @@ private function getTestAppConfig() * @param string $value * @param string $scopeType * @param string|null $scopeCode + * @return void */ - private function persistConfig($path, $value, $scopeType, $scopeCode): void + private function persistConfig(string $path, string $value, string $scopeType, ?string $scopeCode): void { $pathParts = explode('/', $path); $store = 0; - if ($scopeType === \Magento\Store\Model\ScopeInterface::SCOPE_STORE - && $scopeCode !== null) { - $store = ObjectManager::getInstance() - ->get(\Magento\Store\Api\StoreRepositoryInterface::class) - ->get($scopeCode) - ->getId(); - } $configData = [ 'section' => $pathParts[0], 'website' => '', @@ -110,9 +117,15 @@ private function persistConfig($path, $value, $scopeType, $scopeCode): void ] ] ]; - ObjectManager::getInstance() - ->get(\Magento\Config\Model\Config\Factory::class) - ->create(['data' => $configData]) - ->save(); + if ($scopeType === ScopeInterface::SCOPE_STORE && $scopeCode !== null) { + $store = $this->storeRepository->get($scopeCode)->getId(); + $configData['store'] = $store; + } elseif ($scopeType === ScopeInterface::SCOPE_WEBSITES && $scopeCode !== null) { + $website = $this->websiteRepository->get($scopeCode)->getId(); + $configData['store'] = ''; + $configData['website'] = $website; + } + + $this->configFactory->create(['data' => $configData])->save(); } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Bootstrap/WebapiDocBlock.php b/dev/tests/api-functional/framework/Magento/TestFramework/Bootstrap/WebapiDocBlock.php index a3a013ae812ad..7b7047d1aceba 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Bootstrap/WebapiDocBlock.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Bootstrap/WebapiDocBlock.php @@ -32,7 +32,7 @@ protected function _getSubscribers(\Magento\TestFramework\Application $applicati unset($subscribers[$key]); } } - $subscribers[] = new \Magento\TestFramework\Annotation\ApiDataFixture($this->_fixturesBaseDir); + $subscribers[] = new \Magento\TestFramework\Annotation\ApiDataFixture(); $subscribers[] = new ApiConfigFixture(); return $subscribers; 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 new file mode 100644 index 0000000000000..06605d156933d --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override; + +use Magento\Framework\App\ObjectManager; +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; +use Magento\TestFramework\Workaround\Override\Config as IntegrationConfig; + +/** + * Provides api tests configuration. + */ +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, ['types' => $this::FIXTURE_TYPES]); + } + + /** + * @inheritdoc + */ + protected function getSchemaLocator(): SchemaLocatorInterface + { + return ObjectManager::getInstance()->create(SchemaLocator::class); + } + + /** + * @inheritdoc + */ + protected function getFileCollector(): CollectorInterface + { + return ObjectManager::getInstance()->create(FileCollector::class); + } +} 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 new file mode 100644 index 0000000000000..c14e535187296 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override\Config; + +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ApiDataFixture; +use Magento\TestFramework\Annotation\ConfigFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; +use Magento\TestFramework\Workaround\Override\Config\Converter as IntegrationConverter; + +/** + * Converter for api tests config + */ +class Converter extends IntegrationConverter +{ + /** + * Fill node attributes values + * + * @param \DOMElement $fixture + * @return array + */ + protected function fillAttributes(\DOMElement $fixture): array + { + $result = []; + switch ($fixture->nodeName) { + case DataFixtureBeforeTransaction::ANNOTATION: + case DataFixture::ANNOTATION: + case ApiDataFixture::ANNOTATION: + $result = $this->fillDataFixtureAttributes($fixture); + break; + case ConfigFixture::ANNOTATION: + $result = $this->fillConfigFixtureAttributes($fixture); + break; + case AdminConfigFixture::ANNOTATION: + $result = $this->fillAdminConfigFixtureAttributes($fixture); + break; + default: + break; + } + + return $result; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/FileCollector.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/FileCollector.php new file mode 100644 index 0000000000000..29ff24a0c0478 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/FileCollector.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override\Config; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Component\DirSearch; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\File\CollectorInterface; +use Magento\Framework\View\File\Factory as FileFactory; + +class FileCollector implements CollectorInterface +{ + /** + * @var DirSearch + */ + private $componentDirSearch; + + /** + * @var FileFactory + */ + private $fileFactory; + + /** + * @param DirSearch $dirSearch + * @param FileFactory $fileFactory + */ + public function __construct( + DirSearch $dirSearch, + FileFactory $fileFactory + ) { + $this->componentDirSearch = $dirSearch; + $this->fileFactory = $fileFactory; + } + + /** + * Retrieve files + * + * @param \Magento\Framework\View\Design\ThemeInterface $theme + * @param string $filePath + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @return \Magento\Framework\View\File[] + */ + public function getFiles(ThemeInterface $theme, $filePath) + { + $result = []; + $configFiles = $this->componentDirSearch->collectFilesWithContext( + ComponentRegistrar::MODULE, + 'Test/Api/_files/' . $filePath + ); + foreach ($configFiles as $file) { + $result[] = $this->fileFactory->create($file->getFullPath(), $file->getComponentName(), null, true); + } + return $result; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/SchemaLocator.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/SchemaLocator.php new file mode 100644 index 0000000000000..6c671e25d2812 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/SchemaLocator.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override\Config; + +use Magento\Framework\Config\SchemaLocatorInterface; + +/** + * Schema locator for tests config + */ +class SchemaLocator implements SchemaLocatorInterface +{ + /** + * @inheritdoc + */ + public function getSchema() + { + return __DIR__ . '/../../etc/overrides.xsd'; + } + + /** + * @inheritdoc + */ + public function getPerFileSchema() + { + return null; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Fixture/Resolver.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Fixture/Resolver.php new file mode 100644 index 0000000000000..e024e00a7e4e9 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Fixture/Resolver.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\WebapiWorkaround\Override\Fixture; + +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\Workaround\Override\Fixture\Applier\AdminConfigFixture as AdminConfigFixtureApplier; +use Magento\TestFramework\Workaround\Override\Fixture\Applier\ApplierInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Applier\ConfigFixture as ConfigFixtureApplier; +use Magento\TestFramework\Workaround\Override\Fixture\Applier\DataFixture as DataFixtureApplier; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver as IntegrationResolver; + +/** + * Class determines fixture applying according to configurations + */ +class Resolver extends IntegrationResolver +{ + /** + * Get appropriate fixture applier according to fixture type + * + * @param string $fixtureType + * @return ApplierInterface + */ + protected function getApplierByFixtureType(string $fixtureType): ApplierInterface + { + switch ($fixtureType) { + case ApiDataFixture::ANNOTATION: + case DataFixture::ANNOTATION: + case DataFixtureBeforeTransaction::ANNOTATION: + $applier = $this->objectManager->get(DataFixtureApplier::class); + break; + case ApiConfigFixture::ANNOTATION: + $applier = $this->objectManager->get(ConfigFixtureApplier::class); + break; + case AdminConfigFixture::ANNOTATION: + $applier = $this->objectManager->get(AdminConfigFixtureApplier::class); + break; + default: + throw new \InvalidArgumentException(sprintf('Unsupported fixture type %s provided', $fixtureType)); + } + + return $applier; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/etc/overrides.xsd b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/etc/overrides.xsd new file mode 100644 index 0000000000000..c0409afa9ea65 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/etc/overrides.xsd @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="overrides"> + <xs:complexType> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="test" type="test" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:complexType name="test"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="method" type="method" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoApiDataFixture" type="dataFixture" 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:attribute name="class" type="xs:string" use="required"/> + <xs:attribute name="skip" type="xs:boolean"/> + <xs:attribute name="skipMessage" type="xs:string"/> + </xs:complexType> + <xs:complexType name="method"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="dataSet" type="dataSet" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoApiDataFixture" type="dataFixture" 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:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="skip" type="xs:boolean"/> + <xs:attribute name="skipMessage" type="xs:string"/> + </xs:complexType> + <xs:complexType name="dataSet"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="magentoApiDataFixture" type="dataFixture" 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:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="skip" type="xs:boolean"/> + <xs:attribute name="skipMessage" type="xs:string"/> + </xs:complexType> + <xs:complexType name="dataFixture"> + <xs:attribute name="path" type="xs:string" use="required"/> + <xs:attribute name="newPath" type="xs:string"/> + <xs:attribute name="before" type="xs:string"/> + <xs:attribute name="after" type="xs:string"/> + <xs:attribute name="remove" type="xs:boolean"/> + </xs:complexType> + <xs:complexType name="configFixture"> + <xs:attribute name="path" type="xs:string" use="required"/> + <xs:attribute name="scopeType"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="store"/> + <xs:enumeration value="website"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="scopeCode" type="xs:string"/> + <xs:attribute name="value" type="xs:string"/> + <xs:attribute name="newValue" type="xs:string"/> + <xs:attribute name="remove" type="xs:boolean"/> + </xs:complexType> + <xs:complexType name="adminConfigFixture"> + <xs:attribute name="path" type="xs:string" use="required"/> + <xs:attribute name="value" type="xs:string"/> + <xs:attribute name="newValue" type="xs:string"/> + <xs:attribute name="remove" type="xs:boolean"/> + </xs:complexType> +</xs:schema> diff --git a/dev/tests/api-functional/framework/bootstrap.php b/dev/tests/api-functional/framework/bootstrap.php index 01580bc268d05..d3a9a6add5776 100644 --- a/dev/tests/api-functional/framework/bootstrap.php +++ b/dev/tests/api-functional/framework/bootstrap.php @@ -94,9 +94,19 @@ $themePackageList ) ); - unset($bootstrap, $application, $settings, $shell); + $overrideConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Magento\TestFramework\WebapiWorkaround\Override\Config::class + ); + $overrideConfig->init(); + Magento\TestFramework\Workaround\Override\Fixture\Resolver::setInstance( + new \Magento\TestFramework\WebapiWorkaround\Override\Fixture\Resolver($overrideConfig) + ); + \Magento\TestFramework\Workaround\Override\Config::setInstance($overrideConfig); + unset($bootstrap, $application, $settings, $shell, $overrideConfig); } catch (\Exception $e) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $e . PHP_EOL; + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(1); } diff --git a/dev/tests/api-functional/phpunit_graphql.xml.dist b/dev/tests/api-functional/phpunit_graphql.xml.dist index aa1899d88f48e..2f6ad1f9a37d4 100644 --- a/dev/tests/api-functional/phpunit_graphql.xml.dist +++ b/dev/tests/api-functional/phpunit_graphql.xml.dist @@ -13,10 +13,15 @@ columns="max" beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="./framework/bootstrap.php" + testSuiteLoaderClass="Magento\TestFramework\ApiSuiteLoader" + testSuiteLoaderFile="framework/Magento/TestFramework/ApiSuiteLoader.php" > <!-- Test suites definition --> <testsuites> - <testsuite name="Magento GraphQL web API functional tests"> + <testsuite name="Magento GraphQL web API functional tests"> + <file>testsuite/Magento/WebApiTest.php</file> + </testsuite> + <testsuite name="Magento GraphQL web API functional tests real suite"> <directory suffix="Test.php">testsuite/Magento/GraphQl</directory> </testsuite> </testsuites> @@ -47,6 +52,7 @@ <const name="TESTS_MAGENTO_INSTALLATION" value="disabled"/> <!-- Magento mode for tests execution. Possible values are "default", "developer" and "production". --> <const name="TESTS_MAGENTO_MODE" value="default"/> + <const name="USE_OVERRIDE_CONFIG" value="enabled"/> </php> <!-- Test listeners --> diff --git a/dev/tests/api-functional/phpunit_rest.xml.dist b/dev/tests/api-functional/phpunit_rest.xml.dist index c5173e5dd432e..065c2bd11c48c 100644 --- a/dev/tests/api-functional/phpunit_rest.xml.dist +++ b/dev/tests/api-functional/phpunit_rest.xml.dist @@ -13,13 +13,18 @@ columns="max" beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="./framework/bootstrap.php" + testSuiteLoaderClass="Magento\TestFramework\ApiSuiteLoader" + testSuiteLoaderFile="framework/Magento/TestFramework/ApiSuiteLoader.php" > <!-- Test suites definition --> <testsuites> <testsuite name="Magento REST web API functional tests"> + <file>testsuite/Magento/WebApiTest.php</file> + </testsuite> + <testsuite name="Magento REST web API functional tests real suite"> <directory suffix="Test.php">testsuite</directory> - <exclude>testsuite/Magento/GraphQl</exclude> <directory suffix="Test.php">../../../app/code/*/*/Test/Api</directory> + <exclude>testsuite/Magento/GraphQl</exclude> </testsuite> </testsuites> @@ -53,6 +58,7 @@ <const name="TESTS_MAGENTO_INSTALLATION" value="disabled"/> <!-- Magento mode for tests execution. Possible values are "default", "developer" and "production". --> <const name="TESTS_MAGENTO_MODE" value="default"/> + <const name="USE_OVERRIDE_CONFIG" value="enabled"/> </php> <!-- Test listeners --> diff --git a/dev/tests/api-functional/phpunit_soap.xml.dist b/dev/tests/api-functional/phpunit_soap.xml.dist index 935f5113b67a7..5e90b8965d34c 100644 --- a/dev/tests/api-functional/phpunit_soap.xml.dist +++ b/dev/tests/api-functional/phpunit_soap.xml.dist @@ -13,10 +13,15 @@ columns="max" beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="./framework/bootstrap.php" + testSuiteLoaderClass="Magento\TestFramework\ApiSuiteLoader" + testSuiteLoaderFile="framework/Magento/TestFramework/ApiSuiteLoader.php" > <!-- Test suites definition --> <testsuites> <testsuite name="Magento SOAP web API functional tests"> + <file>testsuite/Magento/WebApiTest.php</file> + </testsuite> + <testsuite name="Magento SOAP web API functional tests real suite"> <directory suffix="Test.php">testsuite</directory> <!-- <exclude>testsuite/Magento/GraphQl</exclude> --> <directory suffix="Test.php">../../../app/code/*/*/Test/Api</directory> @@ -52,6 +57,7 @@ <const name="TESTS_MAGENTO_INSTALLATION" value="disabled"/> <!-- Magento mode for tests execution. Possible values are "default", "developer" and "production". --> <const name="TESTS_MAGENTO_MODE" value="default"/> + <const name="USE_OVERRIDE_CONFIG" value="enabled"/> </php> <!-- Test listeners --> 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 6e90e85782bb2..8d6bae35de49b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -7,8 +7,9 @@ namespace Magento\GraphQl\Customer; +use Exception; use Magento\Customer\Model\CustomerAuthUpdate; -use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\AuthenticationException; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -113,7 +114,7 @@ public function testUpdateCustomer() */ public function testUpdateCustomerIfInputDataIsEmpty() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('"input" value should be specified'); $currentEmail = 'customer@example.com'; @@ -139,7 +140,7 @@ public function testUpdateCustomerIfInputDataIsEmpty() */ public function testUpdateCustomerIfUserIsNotAuthorized() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('The current customer isn\'t authorized.'); $newFirstname = 'Richard'; @@ -165,7 +166,7 @@ public function testUpdateCustomerIfUserIsNotAuthorized() */ public function testUpdateCustomerIfAccountIsLocked() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('The account is locked.'); $this->lockCustomer->execute(1); @@ -195,7 +196,7 @@ public function testUpdateCustomerIfAccountIsLocked() */ public function testUpdateEmailIfPasswordIsMissed() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('Provide the current "password" to change "email".'); $currentEmail = 'customer@example.com'; @@ -223,7 +224,7 @@ public function testUpdateEmailIfPasswordIsMissed() */ public function testUpdateEmailIfPasswordIsInvalid() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('Invalid login or password.'); $currentEmail = 'customer@example.com'; @@ -253,8 +254,10 @@ public function testUpdateEmailIfPasswordIsInvalid() */ public function testUpdateEmailIfEmailAlreadyExists() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('A customer with the same email address already exists in an associated website.'); + $this->expectException(Exception::class); + $this->expectExceptionMessage( + 'A customer with the same email address already exists in an associated website.' + ); $currentEmail = 'customer@example.com'; $currentPassword = 'password'; @@ -281,12 +284,42 @@ public function testUpdateEmailIfEmailAlreadyExists() $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateEmailIfEmailIsInvalid() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $invalidEmail = 'customer.example.com'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + email: "{$invalidEmail}" + password: "{$currentPassword}" + } + ) { + customer { + email + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('"' . $invalidEmail . '" is not a valid email address.'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ public function testEmptyCustomerName() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('Required parameters are missing: First Name'); $currentEmail = 'customer@example.com'; @@ -310,10 +343,89 @@ public function testEmptyCustomerName() $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testEmptyCustomerLastName() + { + $query = <<<QUERY +mutation { + updateCustomer( + input: { + lastname: "" + } + ) { + customer { + lastname + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Required parameters are missing: Last Name'); + + $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 + */ + public function testUpdateCustomerIfDobIsInvalid() + { + $invalidDob = 'bla-bla-bla'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + date_of_birth: "{$invalidDob}" + } + ) { + customer { + date_of_birth + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid date'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + /** * @param string $email * @param string $password * @return array + * @throws AuthenticationException */ private function getCustomerAuthHeaders(string $email, string $password): array { 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/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/GiftMessageTest.php new file mode 100644 index 0000000000000..8eaac6d46aa02 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/GiftMessageTest.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\GiftMessage\Cart; + +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_order 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/quote_with_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageForCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('message_order_21'); + $response = $this->requestCartAndAssertResult($maskedQuoteId); + self::assertArrayHasKey('gift_message', $response['cart']); + self::assertSame('Mercutio', $response['cart']['gift_message']['to']); + self::assertSame('Romeo', $response['cart']['gift_message']['from']); + self::assertSame('I thought all for the best.', $response['cart']['gift_message']['message']); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_order 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/quote_with_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageForCartWithNotAllow() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('message_order_21'); + $response = $this->requestCartAndAssertResult($maskedQuoteId); + self::assertArrayHasKey('gift_message', $response['cart']); + self::assertNull($response['cart']['gift_message']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageForCartWithoutMessage() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->requestCartAndAssertResult($maskedQuoteId); + self::assertArrayHasKey('gift_message', $response['cart']); + self::assertNull($response['cart']['gift_message']); + } + + /** + * Get Gift Message Assertion + * + * @param string $quoteId + * + * @return array + * @throws Exception + */ + private function requestCartAndAssertResult(string $quoteId) + { + $query = <<<QUERY +{ + cart(cart_id: "$quoteId") { + gift_message { + to + from + message + } + } +} +QUERY; + return $this->graphQlQuery($query); + } +} 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/GiftMessage/Order/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Order/GiftMessageTest.php new file mode 100644 index 0000000000000..538456884df58 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Order/GiftMessageTest.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\GiftMessage\Order; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class GiftMessageTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp(): void + { + parent::setUp(); + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_order 1 + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GiftMessage/_files/customer/order_with_message.php + * @throws AuthenticationException + * @throws Exception + */ + public function testGiftMessageForOrder() + { + $query = $this->getCustomerOrdersQuery(); + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + foreach ($response['customerOrders']['items'] as $order) { + self::assertArrayHasKey('gift_message', $order); + self::assertArrayHasKey('to', $order['gift_message']); + self::assertArrayHasKey('from', $order['gift_message']); + self::assertArrayHasKey('message', $order['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_order 0 + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GiftMessage/_files/customer/order_with_message.php + */ + public function testGiftMessageNotAllowForOrder() + { + $query = $this->getCustomerOrdersQuery(); + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Can\'t load gift message for order'); + $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Get Customer Orders query + * + * @return string + */ + private function getCustomerOrdersQuery() + { + return <<<QUERY +query { + customerOrders { + items { + order_number + gift_message { + to + from + message + } + } + } +} +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/Newsletter/Customer/SubscribeEmailToNewsletterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php new file mode 100644 index 0000000000000..ec0e49cc55153 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Customer/SubscribeEmailToNewsletterTest.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Newsletter\Customer; + +use Exception; +use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResourceModel; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test newsletter email subscription for customer + */ +class SubscribeEmailToNewsletterTest extends GraphQlAbstract +{ + /** + * @var CustomerAuthUpdate + */ + private $customerAuthUpdate; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var SubscriberResourceModel + */ + private $subscriberResource; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthUpdate = Bootstrap::getObjectManager()->get(CustomerAuthUpdate::class); + $this->customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->subscriberResource = $objectManager->get(SubscriberResourceModel::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testAddRegisteredCustomerEmailIntoNewsletterSubscription() + { + $query = $this->getQuery('customer@example.com'); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('subscribeEmailToNewsletter', $response); + self::assertNotEmpty($response['subscribeEmailToNewsletter']); + self::assertEquals('SUBSCRIBED', $response['subscribeEmailToNewsletter']['status']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testAddLockedCustomerEmailIntoNewsletterSubscription() + { + /* lock customer */ + $customerSecure = $this->customerRegistry->retrieveSecureData(1); + $customerSecure->setLockExpires('2030-12-31 00:00:00'); + $this->customerAuthUpdate->saveAuth(1); + + $query = $this->getQuery('customer@example.com'); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('subscribeEmailToNewsletter', $response); + self::assertNotEmpty($response['subscribeEmailToNewsletter']); + self::assertEquals('SUBSCRIBED', $response['subscribeEmailToNewsletter']['status']); + } + + /** + * @magentoConfigFixture default_store newsletter/subscription/confirm 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testSubscribeRegisteredCustomerEmailWithEnabledConfirmation() + { + $query = $this->getQuery('customer@example.com'); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('subscribeEmailToNewsletter', $response); + self::assertNotEmpty($response['subscribeEmailToNewsletter']); + self::assertEquals('NOT_ACTIVE', $response['subscribeEmailToNewsletter']['status']); + } + + /** + * @magentoConfigFixture default_store customer/create_account/confirm 1 + * @magentoApiDataFixture Magento/Customer/_files/unconfirmed_customer.php + * @expectedException Exception + * @expectedExceptionMessage The account sign-in was incorrect or your account is disabled temporarily. + * Please wait and try again later + */ + public function testNewsletterSubscriptionWithUnconfirmedCustomer() + { + $headers = $this->getHeaderMap('unconfirmedcustomer@example.com', 'Qwert12345'); + $query = $this->getQuery('unconfirmedcustomer@example.com'); + + $this->graphQlMutation($query, [], '', $headers); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testNewsletterSubscriptionWithIncorrectEmailFormat() + { + $query = $this->getQuery('customer.example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Enter a valid email address.' . "\n"); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Newsletter/_files/subscribers.php + */ + public function testNewsletterSubscriptionWithAlreadySubscribedEmail() + { + $query = $this->getQuery('customer@example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('This email address is already subscribed.' . "\n"); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Newsletter/_files/three_subscribers.php + */ + public function testNewsletterSubscriptionWithAnotherCustomerEmail() + { + $query = $this->getQuery('customer2@search.example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Cannot create a newsletter subscription.' . "\n"); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer@search.example.com')); + } + + /** + * Returns a mutation query + * + * @param string $email + * @return string + */ + private function getQuery(string $email = ''): string + { + return <<<QUERY +mutation { + subscribeEmailToNewsletter( + email: "$email" + ) { + status + } +} +QUERY; + } + + /** + * Retrieve customer authorization headers + * + * @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 + ]; + } + + /** + * @inheritDoc + */ + public function tearDown(): void + { + $this->subscriberResource + ->getConnection() + ->delete( + $this->subscriberResource->getMainTable() + ); + + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php new file mode 100644 index 0000000000000..f0a933609c762 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Newsletter/Guest/SubscribeEmailToNewsletterTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Newsletter\Guest; + +use Exception; +use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResourceModel; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test newsletter email subscription for guest + */ +class SubscribeEmailToNewsletterTest extends GraphQlAbstract +{ + /** + * @var SubscriberResourceModel + */ + private $subscriberResource; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->subscriberResource = $objectManager->get(SubscriberResourceModel::class); + } + + public function testAddEmailIntoNewsletterSubscription() + { + $query = $this->getQuery('guest@example.com'); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('subscribeEmailToNewsletter', $response); + self::assertNotEmpty($response['subscribeEmailToNewsletter']); + self::assertEquals('SUBSCRIBED', $response['subscribeEmailToNewsletter']['status']); + } + + public function testNewsletterSubscriptionWithIncorrectEmailFormat() + { + $query = $this->getQuery('guest.example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Enter a valid email address.' . "\n"); + + $this->graphQlMutation($query); + } + + /** + * @magentoConfigFixture default_store newsletter/subscription/allow_guest_subscribe 0 + */ + public function testNewsletterSubscriptionWithDisallowedGuestSubscription() + { + $query = $this->getQuery('guest@example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage( + 'Guests can not subscribe to the newsletter. You must create an account to subscribe.' . "\n" + ); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Newsletter/_files/guest_subscriber.php + */ + public function testNewsletterSubscriptionWithAlreadySubscribedEmail() + { + $query = $this->getQuery('guest@example.com'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('This email address is already subscribed.' . "\n"); + + $this->graphQlMutation($query); + } + + /** + * Returns a mutation query + * + * @param string $email + * @return string + */ + private function getQuery(string $email = ''): string + { + return <<<QUERY +mutation { + subscribeEmailToNewsletter( + email: "$email" + ) { + status + } +} +QUERY; + } + + /** + * @inheritDoc + */ + public function tearDown(): void + { + $this->subscriberResource + ->getConnection() + ->delete( + $this->subscriberResource->getMainTable() + ); + + parent::tearDown(); + } +} 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/SendFriend/SendFriendTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php index 337068710c31b..040215a241c47 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php @@ -122,7 +122,9 @@ public function testSendFriendDisableAsCustomer() public function testSendWithoutExistProduct() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The product that was requested doesn\'t exist. Verify the product and try again.'); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); $productId = 2018; $recipients = '{ @@ -290,81 +292,124 @@ public function testSendProductWithoutVisibility() /** * @return array */ - public function sendFriendsErrorsDataProvider() + public function sendFriendsErrorsDataProvider(): array + { + return array_merge( + $this->getRecipientErrors(), + $this->getSenderErrors() + ); + } + + /** + * @return array + */ + private function getRecipientErrors(): array { return [ [ - 'product_id: 1 - sender: { - name: "Name" - email: "e@mail.com" - message: "Lorem Ipsum" - } - recipients: [ - { - name: "" - email:"recipient1@mail.com" - }, - { - name: "" - email:"recipient2@mail.com" - } - ]', 'Please provide Name for all of recipients.' + 'product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "" + email:"recipient1@mail.com" + }, + { + name: "" + email:"recipient2@mail.com" + } + ]', + 'Please provide Name for all of recipients.' ], [ 'product_id: 1 - sender: { - name: "Name" - email: "e@mail.com" - message: "Lorem Ipsum" - } - recipients: [ - { - name: "Recipient Name 1" - email:"" - }, - { - name: "Recipient Name 2" - email:"" - } - ]', 'Please provide Email for all of recipients.' + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"" + }, + { + name: "Recipient Name 2" + email:"" + } + ]', + 'Please provide Email for all of recipients.' + ], + ]; + } + + /** + * @return array + */ + private function getSenderErrors(): array + { + return [ + [ + 'product_id: 1 + sender: { + name: "" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', + 'Please provide Name of sender.' ], [ 'product_id: 1 - sender: { - name: "" - email: "e@mail.com" - message: "Lorem Ipsum" - } - recipients: [ - { - name: "Recipient Name 1" - email:"recipient1@mail.com" - }, - { - name: "Recipient Name 2" - email:"recipient2@mail.com" - } - ]', 'Please provide Name of sender.' + sender: { + name: "Name" + email: "" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', + 'Please provide Email of sender.' ], [ 'product_id: 1 - sender: { - name: "Name" - email: "e@mail.com" - message: "" - } - recipients: [ - { - name: "Recipient Name 1" - email:"recipient1@mail.com" - }, - { - name: "Recipient Name 2" - email:"recipient2@mail.com" - } - ]', 'Please provide Message.' - ] + sender: { + name: "Name" + email: "e@mail.com" + message: "" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', + 'Please provide Message.' + ], ]; } 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/AbstractOverridesTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/AbstractOverridesTest.php new file mode 100644 index 0000000000000..f0dccff848f04 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/AbstractOverridesTest.php @@ -0,0 +1,38 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig; + +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Base class for override config tests. + */ +abstract class AbstractOverridesTest extends WebapiAbstract +{ + /** @var ObjectManagerInterface */ + protected $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $useConfig = (defined('USE_OVERRIDE_CONFIG') && USE_OVERRIDE_CONFIG === 'enabled'); + + if (!$useConfig) { + $this->markTestSkipped('Override config is disabled.'); + } + + $this->objectManager = Bootstrap::getObjectManager(); + } +} 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/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/AddFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/AddFixtureTest.php new file mode 100644 index 0000000000000..bc9933a886f50 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/AddFixtureTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiConfigFixture; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that magentoConfigFixtures can be added via override config + * + * @magentoAppIsolation enabled + */ +class AddFixtureTest extends AbstractOverridesTest +{ + /** @var ScopeConfigInterface */ + private $config; + + /** @var ConfigStorage */ + private $configStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + } + + /** + * Checks that fixture added in test class node successfully applied + * + * @return void + */ + public function testAddFixtureToClass(): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals('overridden value for full class', $value); + $this->assertEquals( + 'overridden value for full class', + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * Checks that fixtures added in method and data set nodes successfully applied + * + * @dataProvider testDataProvider + * + * @param string $expectedConfigValue + * @return void + */ + public function testAddFixtureToMethod(string $expectedConfigValue): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals($expectedConfigValue, $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => ['expected_config_value' => 'overridden value for method'], + 'second_data_set' => ['expected_config_value' => 'overridden value for data set'] + ]; + } + + /** + * Checks that fixtures can be added on website scope + * + * @return void + */ + public function testAddFixtureOnWebsiteScope(): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_WEBSITES, 'base'); + $this->assertEquals('overridden value for method on website scope', $value); + $this->assertEquals( + 'overridden value for method on website scope', + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_WEBSITES, + 'base' + ) + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/RemoveFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/RemoveFixtureTest.php new file mode 100644 index 0000000000000..148f18b4cc811 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/RemoveFixtureTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiConfigFixture; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that magentoConfigFixtures can be removed using override config + * + * @magentoAppIsolation enabled + */ +class RemoveFixtureTest extends AbstractOverridesTest +{ + /** @var ScopeConfigInterface */ + private $config; + + /** @var ConfigStorage */ + private $configStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + } + + /** + * Checks that fixture can be removed in test class node + * + * @magentoConfigFixture default_store test_section/test_group/field_1 new_value + * + * @return void + */ + public function testRemoveFixtureForClass(): void + { + $value = $this->config->getValue( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ); + $this->assertEquals('1st field default value', $value); + $this->assertFalse( + $this->configStorage->checkIsRecordExist( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * Checks that fixtures can be removed in method and data set nodes + * + * @magentoConfigFixture default_store test_section/test_group/field_2 new_value + * @magentoConfigFixture default_store test_section/test_group/field_3 new_value + * + * @dataProvider testDataProvider + * + * @param string $expectedFirstValue + * @param string $expectedSecondValue + * @param bool $firstvalueExist + * @param bool $secondvalueExist + * @return void + */ + public function testRemoveFixtureForMethod( + string $expectedFirstValue, + string $expectedSecondValue, + bool $firstvalueExist, + bool $secondvalueExist + ): void { + $fistValue = $this->config->getValue( + 'test_section/test_group/field_2', + ScopeInterface::SCOPE_STORES, + 'default' + ); + $secondValue = $this->config->getValue( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_STORES, + 'default' + ); + $this->assertEquals($expectedFirstValue, $fistValue); + if ($firstvalueExist) { + $this->assertEquals( + $expectedFirstValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_2', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + $this->assertEquals( + $firstvalueExist, + $this->configStorage->checkIsRecordExist( + 'test_section/test_group/field_2', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + $this->assertEquals($expectedSecondValue, $secondValue); + if ($secondvalueExist) { + $this->assertEquals( + $expectedSecondValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + $this->assertEquals( + $secondvalueExist, + $this->configStorage->checkIsRecordExist( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => [ + 'expected_first_config_value' => '2nd field default value', + 'expected_second_config_value' => 'new_value', + 'first_value_exist' => false, + 'second_value_exist' => true, + ], + 'second_data_set' => [ + 'expected_first_config_value' => '2nd field default value', + 'expected_second_config_value' => '3rd field website scope default value', + 'first_value_exist' => false, + 'second_value_exist' => false, + ], + ]; + } + + /** + * Checks that website scope fixture can be removed + * + * @magentoConfigFixture base_website test_section/test_group/field_3 new_value + * + * @return void + */ + public function testRemoveWebsiteScopeFixture(): void + { + $value = $this->config->getValue( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_WEBSITES, + 'base' + ); + $this->assertEquals('3rd field website scope default value', $value); + $this->assertFalse( + $this->configStorage->checkIsRecordExist( + 'test_section/test_group/field_3', + ScopeInterface::SCOPE_WEBSITES, + 'base' + ) + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/ReplaceFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/ReplaceFixtureTest.php new file mode 100644 index 0000000000000..d8342fe3394ce --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiConfigFixture/ReplaceFixtureTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiConfigFixture; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class check that fixtures can be replaced using override config + * + * @magentoAppIsolation enabled + */ +class ReplaceFixtureTest extends AbstractOverridesTest +{ + /** @var ScopeConfigInterface */ + private $config; + + /** @var ConfigStorage */ + private $configStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + } + + /** + * Checks that fixture can be replaced in test class node + * + * @magentoConfigFixture default_store test_section/test_group/field_1 new_value + * + * @return void + */ + public function testReplaceFixtureForClass(): void + { + $expectedValue = 'Overridden fixture for class'; + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals($expectedValue, $value); + $this->assertEquals( + $expectedValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * Checks that fixture can be replaced in method and data set nodes + * + * @magentoConfigFixture default_store test_section/test_group/field_1 new_value + * + * @dataProvider testDataProvider + * + * @param string $expectedConfigValue + * @return void + */ + public function testReplaceFixtureForMethod(string $expectedConfigValue): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals($expectedConfigValue, $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => [ + 'expected_config_value' => 'Overridden fixture for method', + ], + 'second_data_set' => [ + 'expected_config_value' => 'Overridden fixture for data set', + ], + ]; + } + + /** + * Checks that website scope fixture can be replaced + * + * @magentoConfigFixture base_website test_section/test_group/field_1 new_value + * + * @return void + */ + public function testReplaceWebsiteScopedFixture(): void + { + $expectedConfigValue = 'Overridden value for website scope'; + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_WEBSITES, 'base'); + $this->assertEquals($expectedConfigValue, $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_WEBSITE, + 'base' + ) + ); + } + + /** + * Checks that replace config from last loaded file will be applied + * + * @magentoConfigFixture default_store test_section/test_group/field_1 new_value + * + * @dataProvider configValuesProvider + * + * @param string $expectedConfigValue + * @return void + */ + public function testReplaceFixtureViaThirdModule(string $expectedConfigValue): void + { + $value = $this->config->getValue('test_section/test_group/field_1', ScopeInterface::SCOPE_STORES, 'default'); + $this->assertEquals($expectedConfigValue, $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb( + 'test_section/test_group/field_1', + ScopeInterface::SCOPE_STORES, + 'default' + ) + ); + } + + /** + * @return array + */ + public function configValuesProvider(): array + { + return [ + 'first_data_set' => [ + 'expected_config_value' => 'Overridden fixture for method from third module', + ], + 'second_data_set' => [ + 'expected_config_value' => 'Overridden fixture for data set from third module', + ], + ]; + } + + /** + * Checks that fixture for global scope can be replaced + * + * @magentoConfigFixture test_section/test_group/field_1 new_value + * + * @return void + */ + public function testReplaceDefaultConfig(): void + { + $expectedConfigValue = 'Overridden value for default scope'; + $value = $this->config->getValue('test_section/test_group/field_1'); + $this->assertEquals('Overridden value for default scope', $value); + $this->assertEquals( + $expectedConfigValue, + $this->configStorage->getValueFromDb('test_section/test_group/field_1') + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/AddFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/AddFixtureTest.php new file mode 100644 index 0000000000000..2021bbf10672b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/AddFixtureTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiDataFixture; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that magentoDataFixtures can be added using override config + * + * @magentoAppIsolation enabled + */ +class AddFixtureTest extends AbstractOverridesTest +{ + /** @var FixtureCallStorage */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * Checks that fixtures added in all nodes successfully applied + * + * @dataProvider addedFixturesProvider + * + * @param array $fixtures + * @return void + */ + public function testAddFixtures(array $fixtures): void + { + foreach ($fixtures as $scope => $fixture) { + $this->assertEquals( + 1, + $this->fixtureCallStorage->getFixturesCount($fixture), + sprintf('Fixture added in %s scope was not called', $scope) + ); + } + } + + /** + * @return array + */ + public function addedFixturesProvider(): array + { + return [ + 'first_data_set' => [ + [ + 'class' => 'fixture1_second_module.php', + 'method' => 'fixture2_second_module.php', + 'data_set' => 'fixture3_second_module.php', + ], + ], + 'second_data_set' => [ + [ + 'class' => 'fixture1_second_module.php', + 'method' => 'fixture2_second_module.php', + ], + ], + ]; + } + + /** + * Checks that same fixture can be added via override config from few files + * + * @return void + */ + public function testAddSameFixtures(): void + { + $this->assertEquals( + 3, + $this->fixtureCallStorage->getFixturesCount('fixture2_second_module.php') + ); + } + + /** + * Checks that fixture which require another fixture can be added using override + * + * @return void + */ + public function testAddFixtureWithRequiredFixture(): void + { + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture_with_required_fixture.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture3_second_module.php')); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/RemoveFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/RemoveFixtureTest.php new file mode 100644 index 0000000000000..7521770873b0a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/RemoveFixtureTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiDataFixture; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Checks that magentoDataFixture can be removed using override config + * + * @magentoAppIsolation enabled + */ +class RemoveFixtureTest extends AbstractOverridesTest +{ + /** @var FixtureCallStorage */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * Checks that fixture can be removed in test class node + * + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * + * @return void + */ + public function testRemoveFixtureForClass(): void + { + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture1_first_module.php')); + } + + /** + * Checks that fixture can be removed in method and data set nodes + * + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * + * @dataProvider testDataProvider + * + * @param string $fixtureName + * @return void + */ + public function testRemoveFixtureForMethod(string $fixtureName): void + { + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount($fixtureName)); + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => ['fixture2_first_module.php'], + 'second_data_set' => ['fixture3_first_module.php'], + ]; + } + + /** + * Checks that same fixtures can be removed few times + * + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * + * @return void + */ + public function testRemoveSameFixtures(): void + { + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture3_first_module.php')); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/ReplaceFixtureTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/ReplaceFixtureTest.php new file mode 100644 index 0000000000000..a1892cf6af32a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/ReplaceFixtureTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiDataFixture; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class check that magentoApiDataFixtures can be replaced using override config + * + * @magentoAppIsolation enabled + */ +class ReplaceFixtureTest extends AbstractOverridesTest +{ + /** @var FixtureCallStorage */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * Checks that fixture can be replaced in test class node + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * + * @return void + */ + public function testReplaceFixtureForClass(): void + { + $this->assertEquals(0, $this->fixtureCallStorage->getFixturesCount('fixture1_first_module.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture1_second_module.php')); + } + + /** + * Checks that fixture can be replaced in method and data set nodes + * + * @dataProvider replacedFixturesProvider + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * + * @param string $fixture + * @return void + */ + public function testReplaceFixturesForMethod(string $fixture): void + { + $this->assertEquals(0, $this->fixtureCallStorage->getFixturesCount('fixture1_first_module.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + + /** + * @return array + */ + public function replacedFixturesProvider(): array + { + return [ + 'first_data_set' => [ + 'fixture2_second_module.php', + ], + 'second_data_set' => [ + 'fixture3_second_module.php', + ], + ]; + } + + /** + * Checks that replace config from last loaded file will be applied + * + * @dataProvider dataProvider + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * + * @param string $fixture + * @return void + */ + public function testReplaceFixtureViaThirdModule(string $fixture): void + { + $this->assertEquals(0, $this->fixtureCallStorage->getFixturesCount('fixture1_first_module.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + + /** + * @return array + */ + public function dataProvider(): array + { + return [ + 'first_data_set' => [ + 'fixture2_second_module.php', + ], + 'second_data_set' => [ + 'fixture3_second_module.php', + ], + ]; + } + + /** + * Checks that fixture required in the another fixture can be replaced using override + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig2/_files/fixture_with_required_fixture.php + * + * @return void + */ + public function testReplaceRequiredFixture(): void + { + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture_with_required_fixture.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture2_second_module.php')); + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture3_second_module.php')); + } + + /** + * Checks that fixture required in the another fixture will be replaced according to last loaded override + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig2/_files/fixture_with_required_fixture.php + * + * @return void + */ + public function testReplaceRequiredFixtureViaThirdModule(): void + { + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture_with_required_fixture.php')); + $this->assertEquals(1, $this->fixtureCallStorage->getFixturesCount('fixture1_third_module.php')); + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture2_second_module.php')); + $this->assertEmpty($this->fixtureCallStorage->getFixturesCount('fixture3_second_module.php')); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/SortFixturesTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/SortFixturesTest.php new file mode 100644 index 0000000000000..dc5b7d9e167a5 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/MagentoApiDataFixture/SortFixturesTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\MagentoApiDataFixture; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that magentoConfigFixtures can be placed into certain place using override config + * + * @magentoAppIsolation enabled + */ +class SortFixturesTest extends AbstractOverridesTest +{ + /** @var FixtureCallStorage */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * Checks that fixtures can be placed to specific place according to config + * + * @dataProvider sortFixturesProvider + * + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * + * @param array $sortedFixtures + * @return void + */ + public function testSortFixtures(array $sortedFixtures): void + { + $this->assertEquals($sortedFixtures, $this->fixtureCallStorage->getStorage()); + } + + /** + * @return array + */ + public function sortFixturesProvider(): array + { + return [ + 'first_data_set' => [ + 'sorted_fixtures' => [ + 'fixture3_second_module.php', + 'fixture1_first_module.php', + 'fixture1_second_module.php', + 'fixture2_first_module.php', + 'fixture1_third_module.php', + 'fixture3_first_module.php', + 'fixture2_second_module.php', + ], + ], + 'second_data_set' => [ + 'sorted_fixtures' => [ + 'fixture1_first_module.php', + 'fixture1_second_module.php', + 'fixture2_first_module.php', + 'fixture3_first_module.php', + 'fixture2_second_module.php', + ], + ], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipClassTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipClassTest.php new file mode 100644 index 0000000000000..05f6648c0559a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipClassTest.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that full test class can be skipped + * + * @magentoAppIsolation enabled + */ +class SkipClassTest extends AbstractOverridesTest +{ + /** + * This test should not be executed according to override config it should be mark as skipped + * + * @return void + */ + public function testClassSkip(): void + { + $this->fail('This test should be skipped via override config in test class node'); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipDataSetTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipDataSetTest.php new file mode 100644 index 0000000000000..fec1caa5370e7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipDataSetTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that only specific data set can be skipped using override config + * + * @magentoAppIsolation enabled + */ +class SkipDataSetTest extends AbstractOverridesTest +{ + /** + * The first_data_set should not be executed according to override config it should be mark as skipped + * + * @dataProvider testDataProvider + * + * @return void + */ + public function testSkipDataSet(): void + { + if ($this->dataName() === 'first_data_set') { + $this->fail('This test should be skipped via override config in data set node'); + } + } + + /** + * @return array + */ + public function testDataProvider(): array + { + return [ + 'first_data_set' => [], + 'second_data_set' => [], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipMethodTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipMethodTest.php new file mode 100644 index 0000000000000..f824755650e1a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Skip/SkipMethodTest.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Class checks that test method can be skipped using override config + * + * @magentoAppIsolation enabled + */ +class SkipMethodTest extends AbstractOverridesTest +{ + /** + * This test should not be executed according to override config it should be mark as skipped + * + * @return void + */ + public function testMethodSkip(): void + { + $this->fail('This test should be skipped via override config in method node'); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/WebApiTest.php b/dev/tests/api-functional/testsuite/Magento/WebApiTest.php new file mode 100644 index 0000000000000..32670dfeb7b1b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/WebApiTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento; + +use Magento\TestFramework\SkippableInterface; +use Magento\TestFramework\Workaround\Override\Config; +use Magento\TestFramework\Workaround\Override\WrapperGenerator; +use PHPUnit\Framework\TestSuite; +use PHPUnit\TextUI\Configuration\Registry; +use PHPUnit\TextUI\Configuration\TestSuiteCollection; +use PHPUnit\TextUI\Configuration\TestSuiteMapper; + +/** + * Web API tests wrapper. + */ +class WebApiTest extends TestSuite +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param string $className + * @return TestSuite + */ + public static function suite($className) + { + $generator = new WrapperGenerator(); + $overrideConfig = Config::getInstance(); + $configuration = Registry::getInstance()->get(self::getConfigurationFile()); + $suitesConfig = $configuration->testSuite(); + $suite = new TestSuite(); + /** @var \PHPUnit\TextUI\Configuration\TestSuite $suiteConfig */ + foreach ($suitesConfig as $suiteConfig) { + $suites = (new TestSuiteMapper())->map(TestSuiteCollection::fromArray([$suiteConfig]), ''); + /** @var TestSuite $testSuite */ + foreach ($suites as $testSuite) { + /** @var TestSuite $test */ + foreach ($testSuite as $test) { + $testName = $test->getName(); + + if ($overrideConfig->hasSkippedTest($testName) && !$test instanceof SkippableInterface) { + $reflectionClass = new \ReflectionClass($testName); + $resultTest = $generator->generateTestWrapper($reflectionClass); + $suite->addTest(new TestSuite($resultTest, $testName)); + } else { + $suite->addTest($test); + } + } + } + } + + return $suite; + } + + /** + * Returns config file name from command line params. + * + * @return string + */ + private static function getConfigurationFile(): string + { + $params = getopt('c:', ['configuration:']); + $longConfig = $params['configuration'] ?? ''; + $shortConfig = $params['c'] ?? ''; + + return $shortConfig ? $shortConfig : $longConfig; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php index daf4f7284b2d8..aa21cba143841 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/Model/FixtureCallStorage.php @@ -10,6 +10,8 @@ /** * Class represent simple container to save data + * + * phpcs:disable Generic.Classes.DuplicateClassName */ class FixtureCallStorage { @@ -30,11 +32,11 @@ public function addFixtureToStorage(string $fixture): void * Get fixture position in storage * * @param string $fixture - * @return false|int + * @return null|int */ - public function getFixturePosition(string $fixture) + public function getFixturePosition(string $fixture): ?int { - return array_search($fixture, $this->storage); + return array_search($fixture, $this->storage) ?: null; } /** diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/composer.json b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/composer.json index 85dfc1f4499e6..47ac2d4ac4a3b 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/composer.json +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/composer.json @@ -5,7 +5,7 @@ "sort-packages": true }, "require": { - "php": "~7.1.3||~7.2.0||~7.3.0", + "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-integration": "*" }, 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 3d0db9f3c4283..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 @@ -152,12 +152,13 @@ <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> <dataSet name="first_data_set"> - <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> - <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> </dataSet> </method> <method name="testReplaceRequiredFixture"> <magentoDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> </method> </test> <test class="Magento\TestModuleOverrideConfig\MagentoDataFixture\SortFixturesTest"> @@ -203,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/TestModuleOverrideConfig2/composer.json b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/composer.json index 315db25a09731..43b7bec56945d 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/composer.json +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/composer.json @@ -5,7 +5,7 @@ "sort-packages": true }, "require": { - "php": "~7.1.3||~7.2.0||~7.3.0", + "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-integration": "*" }, 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 fd22cc21f6c6a..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 @@ -35,12 +35,15 @@ <test class="Magento\TestModuleOverrideConfig\MagentoDataFixture\ReplaceFixtureTest"> <method name="testReplaceFixtureViaThirdModule"> <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" /> <dataSet name="first_data_set"> <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> </dataSet> </method> <method name="testReplaceRequiredFixtureViaThirdModule"> <magentoDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module.php" newPath="Magento/TestModuleOverrideConfig3/_files/fixture1_third_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig2/_files/fixture3_second_module_rollback.php" newPath="Magento/TestModuleOverrideConfig3/_files/fixture1_third_module_rollback.php" /> </method> </test> <test class="Magento\TestModuleOverrideConfig\MagentoDataFixture\SortFixturesTest"> @@ -50,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/_files/Magento/TestModuleOverrideConfig3/composer.json b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/composer.json index 6ada46e46fbe3..432b2ef703a57 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/composer.json +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/composer.json @@ -5,7 +5,7 @@ "sort-packages": true }, "require": { - "php": "~7.1.3||~7.2.0||~7.3.0", + "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-integration": "*" }, diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php index a835e73cce826..2f4b7bf79c1d6 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php @@ -107,7 +107,6 @@ protected function _applyOneFixture($fixture) * * @param array $fixtures * @return void - * @throws LocalizedException */ protected function _applyFixtures(array $fixtures) { diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/ConfigFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/ConfigFixture.php index e2c54584db41d..4c1e31c85ec77 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/ConfigFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/ConfigFixture.php @@ -35,26 +35,21 @@ class ConfigFixture * * @var array */ - private $globalConfigValues = []; + protected $globalConfigValues = []; /** * Original values for website-scoped configuration options that need to be restored * * @var array */ - private $websiteConfigValues = []; + protected $websiteConfigValues = []; /** * Original values for store-scoped configuration options that need to be restored * * @var array */ - private $storeConfigValues = []; - - /** - * @var string - */ - protected $annotation = 'magentoConfigFixture'; + protected $storeConfigValues = []; /** * Retrieve configuration node value @@ -164,33 +159,66 @@ protected function _assignConfigData(TestCase $test) ); foreach ($testAnnotations as $configPathAndValue) { if (preg_match('/^.+?(?=_store\s)/', $configPathAndValue, $matches)) { - /* Store-scoped config value */ - $storeCode = $matches[0] != 'current' ? $matches[0] : null; - $parts = preg_split('/\s+/', $configPathAndValue, 3); - list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; - $originalValue = $this->_getConfigValue($configPath, $storeCode); - $this->storeConfigValues[$storeCode][$configPath] = $originalValue; - $this->_setConfigValue($configPath, $requiredValue, $storeCode); + $this->setStoreConfigValue($matches ?? [], $configPathAndValue); } elseif (preg_match('/^.+?(?=_website\s)/', $configPathAndValue, $matches)) { - /* Website-scoped config value */ - $websiteCode = $matches[0] != 'current' ? $matches[0] : null; - $parts = preg_split('/\s+/', $configPathAndValue, 3); - list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; - $originalValue = $this->getScopeConfigValue($configPath, ScopeInterface::SCOPE_WEBSITES, $websiteCode); - $this->websiteConfigValues[$websiteCode][$configPath] = $originalValue; - $this->setScopeConfigValue($configPath, $requiredValue, ScopeInterface::SCOPE_WEBSITES, $websiteCode); + $this->setWebsiteConfigValue($matches ?? [], $configPathAndValue); } else { - /* Global config value */ - list($configPath, $requiredValue) = preg_split('/\s+/', $configPathAndValue, 2); - - $originalValue = $this->_getConfigValue($configPath); - $this->globalConfigValues[$configPath] = $originalValue; - - $this->_setConfigValue($configPath, $requiredValue); + $this->setGlobalConfigValue($configPathAndValue); } } } + /** + * Sets store-scoped config value + * + * @param array $matches + * @param string $configPathAndValue + * @return void + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + protected function setStoreConfigValue(array $matches, $configPathAndValue): void + { + $storeCode = $matches[0] != 'current' ? $matches[0] : null; + $parts = preg_split('/\s+/', $configPathAndValue, 3); + list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; + $originalValue = $this->_getConfigValue($configPath, $storeCode); + $this->storeConfigValues[$storeCode][$configPath] = $originalValue; + $this->_setConfigValue($configPath, $requiredValue, $storeCode); + } + + /** + * Sets website-scoped config value + * + * @param array $matches + * @param string $configPathAndValue + * @return void + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + protected function setWebsiteConfigValue(array $matches, $configPathAndValue): void + { + $websiteCode = $matches[0] != 'current' ? $matches[0] : null; + $parts = preg_split('/\s+/', $configPathAndValue, 3); + list($configScope, $configPath, $requiredValue) = $parts + ['', '', '']; + $originalValue = $this->getScopeConfigValue($configPath, ScopeInterface::SCOPE_WEBSITES, $websiteCode); + $this->websiteConfigValues[$websiteCode][$configPath] = $originalValue; + $this->setScopeConfigValue($configPath, $requiredValue, ScopeInterface::SCOPE_WEBSITES, $websiteCode); + } + + /** + * Sets global config value + * + * @param string $configPathAndValue + * @return void + */ + protected function setGlobalConfigValue($configPathAndValue): void + { + /* Global config value */ + list($configPath, $requiredValue) = preg_split('/\s+/', $configPathAndValue, 2); + $originalValue = $this->_getConfigValue($configPath); + $this->globalConfigValues[$configPath] = $originalValue; + $this->_setConfigValue($configPath, $requiredValue); + } + /** * Restore original values for changed config options * diff --git a/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php b/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php new file mode 100644 index 0000000000000..7fe25f3a6f61c --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Config\Model; + +use Magento\Framework\App\Config\ConfigResource\ConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Class checks config table data using direct calls + */ +class ConfigStorage +{ + /** @var ConfigInterface */ + private $configResource; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @param ConfigInterface $configResource + * @param StoreManagerInterface $storeManager + */ + public function __construct(ConfigInterface $configResource, StoreManagerInterface $storeManager) + { + $this->configResource = $configResource; + $this->storeManager = $storeManager; + } + + /** + * Get value from db + * + * @param string $path + * @param string $scope + * @param string|null $scopeCode + * @return string|false + */ + public function getValueFromDb( + string $path, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + ?string $scopeCode = null + ) { + $connect = $this->configResource->getConnection(); + $scope = $this->normalizeScope($scope); + $scopeId = $this->getIdByScope($scope, $scopeCode); + + $select = $connect->select()->from(['main_table' => $this->configResource->getMainTable()], 'value') + ->where('main_table.path = ?', $path) + ->where('main_table.scope = ?', $scope) + ->where('main_table.scope_id = ?', $scopeId); + + return $connect->fetchOne($select); + } + + /** + * Check is record exist in DB + * + * @param string $path + * @param string $scope + * @param string|null $scopeCode + * @return bool + */ + public function checkIsRecordExist( + string $path, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + ?string $scopeCode = null + ): bool { + $connect = $this->configResource->getConnection(); + $scope = $this->normalizeScope($scope); + $scopeId = $this->getIdByScope($scope, $scopeCode); + + $select = $connect->select()->from(['main_table' => $this->configResource->getMainTable()], 'COUNT(*)') + ->where('main_table.path = ?', $path) + ->where('main_table.scope = ?', $scope) + ->where('main_table.scope_id = ?', $scopeId); + + return (bool)$connect->fetchOne($select); + } + + /** + * Get scope id by scope code + * + * @param string $scope + * @param string|null $scopeCode + * @return int + */ + private function getIdByScope(string $scope, ?string $scopeCode): int + { + $scopeId = 0; + + if ($scope === ScopeInterface::SCOPE_WEBSITES) { + $scopeId = (int)$this->storeManager->getWebsite($scopeCode)->getId(); + } elseif ($scope === ScopeInterface::SCOPE_STORES) { + $scopeId = (int)$this->storeManager->getStore($scopeCode)->getId(); + } + + return $scopeId; + } + + /** + * Normalize scope + * + * @param string $scope + * @return string + */ + private function normalizeScope(string $scope): string + { + if ($scope === ScopeInterface::SCOPE_WEBSITE) { + $scope = ScopeInterface::SCOPE_WEBSITES; + } + if ($scope === ScopeInterface::SCOPE_STORE) { + $scope = ScopeInterface::SCOPE_STORES; + } + + return $scope; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Customer/Model/DeleteCustomer.php b/dev/tests/integration/framework/Magento/TestFramework/Customer/Model/DeleteCustomer.php new file mode 100644 index 0000000000000..5158191efef22 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Customer/Model/DeleteCustomer.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Customer\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; + +/** + * Delete customer by email or id + */ +class DeleteCustomer +{ + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var Registry */ + private $registry; + + /** + * @param CustomerRepositoryInterface $customerRepository + * @param Registry $registry + */ + public function __construct(CustomerRepositoryInterface $customerRepository, Registry $registry) + { + $this->customerRepository = $customerRepository; + $this->registry = $registry; + } + + /** + * Delete customer by id or email + * + * @param int|string $id + * @return void + */ + public function execute($id): void + { + $isSecure = $this->registry->registry('isSecureArea'); + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + try { + $customer = is_numeric($id) ? $this->customerRepository->getById($id) : $this->customerRepository->get($id); + $this->customerRepository->delete($customer); + } catch (NoSuchEntityException $e) { + //customer already deleted + } + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', $isSecure); + } +} 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 5d2d0e385b9e3..f34eec274873d 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php @@ -8,13 +8,22 @@ namespace Magento\TestFramework\Workaround\Override; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\ConverterInterface; use Magento\Framework\Config\Reader\Filesystem; +use Magento\Framework\Config\SchemaLocatorInterface; +use Magento\Framework\Config\ValidationStateInterface; 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\Dom; +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; @@ -24,10 +33,20 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Config +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; @@ -37,152 +56,136 @@ class Config private $config; /** - * @param array $config + * @var array */ - public function __construct(array $config) - { - $this->config = $config; - } + private $inheritedConfig; /** - * Returns an array with skip key and skipMessage key if test is skipped. + * Self instance getter. * - * @param TestCase $test - * @return array + * @return ConfigInterface */ - public function getSkipConfiguration(TestCase $test): array + public static function getInstance(): ConfigInterface { - $classConfig = $this->getClassConfig($test); - $testConfig = $this->getMethodConfig($test); - $dataSetConfig = $this->getDataSetConfig($test); - $result['skip'] = false; - - if (isset($dataSetConfig['skip']) && $dataSetConfig['skip']) { - $result = $this->prepareSkipConfig($dataSetConfig); - } elseif (isset($testConfig['skip']) && $testConfig['skip']) { - $result = $this->prepareSkipConfig($testConfig); - } elseif (isset($classConfig['skip']) && $classConfig['skip']) { - $result = $this->prepareSkipConfig($classConfig); + if (empty(self::$instance)) { + throw new \RuntimeException('Override config isn\'t initialized'); } - return $result; + return self::$instance; } /** - * Test has configuration flag. + * Get config from global node * - * @param string $className - * @return bool + * @param string|null $fixtureType + * @return array */ - public function hasSkippedTest(string $className): bool + public function getGlobalConfig(?string $fixtureType = null): array { - $classConfig = $this->config[$className] ?? []; + $result = $this->config['global'] ?? []; + if ($fixtureType) { + $result = $result[$fixtureType] ?? []; + } - return $this->isSkippedByConfig($classConfig); + return $result; } /** - * Check that class has even one test skipped + * Self instance setter. * - * @param array $config - * @return bool + * @param ConfigInterface $config + * @return void */ - private function isSkippedByConfig(array $config): bool + public static function setInstance(ConfigInterface $config): void { - if (isset($config['skip']) && $config['skip']) { - return true; - } - - foreach ($config as $lowerLevelConfig) { - if (is_array($lowerLevelConfig)) { - return $this->isSkippedByConfig($lowerLevelConfig); - } - } - - return false; + self::$instance = $config; } /** - * Self instance getter. + * Reads configuration from files. * - * @return static + * @return void */ - public static function getInstance(): self + public function init(): void { - if (empty(self::$instance)) { + if (empty($this->config)) { $data = []; - $objectManager = ObjectManager::getInstance(); $useConfig = (defined('USE_OVERRIDE_CONFIG') && USE_OVERRIDE_CONFIG === 'enabled'); if ($useConfig) { - $fileResolver = $objectManager->create( - FileResolver::class, - [ - 'baseFiles' => $objectManager->create( - ModuleDependency::class, - [ - 'subject' => $objectManager->create( - ModuleOutput::class, - [ - 'subject' => $objectManager->create(FileCollector::class) - ] - ) - ] - ) - ] - ); - $reader = $objectManager->create( + $reader = ObjectManager::getInstance()->create( Filesystem::class, [ 'fileName' => 'overrides.xml', - 'fileResolver' => $fileResolver, + 'fileResolver' => $this->getFileResolver(), 'idAttributes' => [ '/overrides/test' => 'class', '/overrides/test/method' => 'name', '/overrides/test/method/dataSet' => 'name', ], - 'schemaLocator' => $objectManager->create(SchemaLocator::class), - 'validationState' => $objectManager->create(ValidationState::class), - 'converter' => $objectManager->create(Converter::class), - 'domDocumentClass' => Dom::class, + 'schemaLocator' => $this->getSchemaLocator(), + 'validationState' => $this->getValidationState(), + 'converter' => $this->getConverter(), + 'domDocumentClass' => $this->getDomClass(), ] ); $data = $reader->read(); } - self::$instance = new self($data); + $this->config = $data; } - - return self::$instance; } /** - * Get config from class node - * - * @param TestCase $test - * @param string|null $fixtureType - * @return array + * @inheritdoc */ - public function getClassConfig(TestCase $test, ?string $fixtureType = null): array + public function getSkipConfiguration(TestCase $test): array { - $result = $this->config[$this->getOriginalClassName($test)] ?? []; - if ($fixtureType) { - $result = $result[$fixtureType] ?? []; + $classConfig = $this->getClassConfig($test); + $testConfig = $this->getMethodConfig($test); + $dataSetConfig = $this->getDataSetConfig($test); + $result['skip'] = false; + + if (isset($dataSetConfig['skip']) && $dataSetConfig['skip']) { + $result = $this->prepareSkipConfig($dataSetConfig); + } elseif (isset($testConfig['skip']) && $testConfig['skip']) { + $result = $this->prepareSkipConfig($testConfig); + } elseif (isset($classConfig['skip']) && $classConfig['skip']) { + $result = $this->prepareSkipConfig($classConfig); } return $result; } /** - * Get config from method node - * - * @param TestCase $test - * @param string|null $fixtureType - * @return array + * @inheritdoc + */ + public function hasSkippedTest(string $className): bool + { + $classConfig = $this->getInheritedClassConfig($className); + + return $this->isSkippedByConfig($classConfig); + } + + /** + * @inheritdoc + */ + public function getClassConfig(TestCase $test, ?string $fixtureType = null): array + { + $config = $this->getInheritedClassConfig($this->getOriginalClassName($test)); + + return $fixtureType + ? $config[$fixtureType] ?? [] + : $config; + } + + /** + * @inheritdoc */ public function getMethodConfig(TestCase $test, ?string $fixtureType = null): array { $config = $this->getClassConfig($test)[$test->getName(false)] ?? []; + if ($fixtureType) { $config = $config[$fixtureType] ?? []; } @@ -191,11 +194,7 @@ public function getMethodConfig(TestCase $test, ?string $fixtureType = null): ar } /** - * Get config from dataSet node - * - * @param TestCase $test - * @param string|null $fixtureType - * @return array + * @inheritdoc */ public function getDataSetConfig(TestCase $test, ?string $fixtureType = null): array { @@ -207,6 +206,106 @@ public function getDataSetConfig(TestCase $test, ?string $fixtureType = null): a return $config; } + /** + * Returns file resolver. + * + * @return FileResolver + */ + protected function getFileResolver(): FileResolver + { + return ObjectManager::getInstance()->create( + FileResolver::class, + [ + 'baseFiles' => ObjectManager::getInstance()->create( + ModuleDependency::class, + [ + 'subject' => ObjectManager::getInstance()->create( + ModuleOutput::class, + [ + 'subject' => $this->getFileCollector() + ] + ) + ] + ) + ] + ); + } + + /** + * Returns schema locator. + * + * @return SchemaLocatorInterface + */ + protected function getSchemaLocator(): SchemaLocatorInterface + { + return ObjectManager::getInstance()->create(SchemaLocator::class); + } + + /** + * Returns validation state. + * + * @return ValidationStateInterface + */ + protected function getValidationState(): ValidationStateInterface + { + return ObjectManager::getInstance()->create(ValidationState::class); + } + + /** + * Returns converter for config files. + * + * @return ConverterInterface + */ + protected function getConverter(): ConverterInterface + { + return ObjectManager::getInstance()->create(Converter::class, ['types' => $this::FIXTURE_TYPES]); + } + + /** + * Returns DOM class name. + * + * @return string + */ + protected function getDomClass(): string + { + return Dom::class; + } + + /** + * Returns file collector. + * + * @return CollectorInterface + */ + protected function getFileCollector(): CollectorInterface + { + return ObjectManager::getInstance()->create(FileCollector::class); + } + + /** + * Check that class has even one test skipped + * + * @param array $config + * @return bool + */ + private function isSkippedByConfig(array $config): bool + { + $result = false; + if (isset($config['skip']) && $config['skip']) { + $result = true; + } else { + foreach ($config as $lowerLevelConfig) { + if (is_array($lowerLevelConfig)) { + $result = $this->isSkippedByConfig($lowerLevelConfig); + if ($result === true) { + break; + } + } + } + } + + return $result; + } + /** * Returns original test class name. * @@ -231,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 571bcdd7007bf..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 { - private 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); @@ -46,7 +52,7 @@ public function convert($source) foreach ($this->xpath->query('./dataSet', $method) as $dataSet) { $setName = $dataSet->getAttribute('name'); - $config[$className][$methodName][$setName] = $config[$className][$methodName][$setName] ?? []; + $config[$className][$methodName][$setName] = $config[$className][$methodName][$setName] ?? []; $config[$className][$methodName][$setName] = $this->fillSkipSection( $dataSet, $config[$className][$methodName][$setName] @@ -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 (self::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); @@ -111,7 +118,7 @@ private function getTestConfigByFixtureType(\DOMElement $node): array * @param \DOMElement $fixture * @return array */ - private function fillAttributes(\DOMElement $fixture): array + protected function fillAttributes(\DOMElement $fixture): array { $result = []; switch ($fixture->nodeName) { @@ -138,7 +145,7 @@ private function fillAttributes(\DOMElement $fixture): array * @param \DOMElement $fixture * @return array */ - private function fillDataFixtureAttributes(\DOMElement $fixture): array + protected function fillDataFixtureAttributes(\DOMElement $fixture): array { return [ 'path' => $fixture->getAttribute('path'), @@ -155,7 +162,7 @@ private function fillDataFixtureAttributes(\DOMElement $fixture): array * @param \DOMElement $fixture * @return array */ - private function fillConfigFixtureAttributes(\DOMElement $fixture): array + protected function fillConfigFixtureAttributes(\DOMElement $fixture): array { return [ 'path' => $fixture->getAttribute('path'), @@ -173,7 +180,7 @@ private function fillConfigFixtureAttributes(\DOMElement $fixture): array * @param \DOMElement $fixture * @return array */ - private function fillAdminConfigFixtureAttributes(\DOMElement $fixture): array + protected function fillAdminConfigFixtureAttributes(\DOMElement $fixture): array { return [ 'path' => $fixture->getAttribute('path'), @@ -182,4 +189,36 @@ private 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/ConfigInterface.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/ConfigInterface.php new file mode 100644 index 0000000000000..dc5e885dcacc1 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/ConfigInterface.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Workaround\Override; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\Reader\Filesystem; +use Magento\Framework\View\File\Collector\Decorator\ModuleDependency; +use Magento\Framework\View\File\Collector\Decorator\ModuleOutput; +use Magento\TestFramework\Workaround\Override\Config\Converter; +use Magento\TestFramework\Workaround\Override\Config\FileCollector; +use Magento\TestFramework\Workaround\Override\Config\FileResolver; +use Magento\TestFramework\Workaround\Override\Config\Dom; +use Magento\TestFramework\Workaround\Override\Config\SchemaLocator; +use Magento\TestFramework\Workaround\Override\Config\ValidationState; +use PHPUnit\Framework\TestCase; + +/** + * Provides integration tests configuration. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +interface ConfigInterface +{ + /** + * Returns an array with skip key and skipMessage key if test is skipped. + * + * @param TestCase $test + * @return array + */ + public function getSkipConfiguration(TestCase $test): array; + + /** + * Test has configuration flag. + * + * @param string $className + * @return bool + */ + public function hasSkippedTest(string $className): bool; + + /** + * Get config from class node + * + * @param TestCase $test + * @param string|null $fixtureType + * @return array + */ + public function getClassConfig(TestCase $test, ?string $fixtureType = null): array; + + /** + * Get config from method node + * + * @param TestCase $test + * @param string|null $fixtureType + * @return array + */ + public function getMethodConfig(TestCase $test, ?string $fixtureType = null): array; + + /** + * Get config from dataSet node + * + * @param TestCase $test + * @param string|null $fixtureType + * @return array + */ + public function getDataSetConfig(TestCase $test, ?string $fixtureType = null): array; +} 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/Applier/ConfigFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/ConfigFixture.php index 7ea04fce47b1c..11c5692ecfbfe 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/ConfigFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/ConfigFixture.php @@ -7,6 +7,8 @@ namespace Magento\TestFramework\Workaround\Override\Fixture\Applier; +use Magento\Framework\App\Config\ScopeConfigInterface; + /** * Class represent config fixtures applying logic */ @@ -63,9 +65,21 @@ protected function initConfigFixture(array $attributes): string { $value = !empty($attributes['newValue']) ? $attributes['newValue'] : $attributes['value']; - return $attributes['scopeType'] === 'default' - ? sprintf('%s/%s %s', $attributes['scopeType'], $attributes['path'], $value) - : sprintf('%s_%s %s %s', $attributes['scopeCode'], $attributes['scopeType'], $attributes['path'], $value); + if (empty($attributes['scopeType'])) { + $result = sprintf('%s %s', $attributes['path'], $value); + } elseif ($attributes['scopeType'] === ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { + $result = sprintf('%s/%s %s', $attributes['scopeType'], $attributes['path'], $value); + } else { + $result = sprintf( + '%s_%s %s %s', + $attributes['scopeCode'], + $attributes['scopeType'], + $attributes['path'], + $value + ); + } + + return $result; } /** diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/DataFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/DataFixture.php index b0bcbc35e5f1d..efd92b46a2bf7 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/DataFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/DataFixture.php @@ -7,6 +7,8 @@ namespace Magento\TestFramework\Workaround\Override\Fixture\Applier; +use Magento\Framework\Exception\LocalizedException; + /** * Class represent data fixtures applying logic */ @@ -104,7 +106,7 @@ private function sortFixtures(array $fixtures, array $attributes): array $beforeFixtures = []; $afterFixtures = []; if (!empty($attributes['before'])) { - $offset = array_search($attributes['before'], $fixtures); + $offset = $this->getFixturePosition($attributes['before'], $fixtures); if ($attributes['before'] === '-' || $offset === 0) { $beforeFixtures[] = $attributes['path']; } else { @@ -115,7 +117,7 @@ private function sortFixtures(array $fixtures, array $attributes): array if ($attributes['after'] === '-') { $afterFixtures[] = $attributes['path']; } else { - $offset = array_search($attributes['after'], $fixtures); + $offset = $this->getFixturePosition($attributes['after'], $fixtures); $fixtures = $this->insertFixture($fixtures, $attributes['path'], $offset + 1); } } elseif (empty($attributes['before'])) { @@ -125,6 +127,27 @@ private function sortFixtures(array $fixtures, array $attributes): array return array_merge($beforeFixtures, $fixtures, $afterFixtures); } + /** + * Get fixture position in added fixtures list + * + * @param string $fixtureToFind + * @param array $existingFixtures + * @return int + * @throws LocalizedException if fixture which have to be found does not exist in added fixtures list + */ + private function getFixturePosition(string $fixtureToFind, array $existingFixtures): int + { + $offset = 0; + if ($fixtureToFind !== '-') { + $offset = array_search($fixtureToFind, $existingFixtures); + if ($offset === false) { + throw new LocalizedException(__('The fixture %1 does not exist in fixtures list', $fixtureToFind)); + } + } + + return $offset; + } + /** * Insert fixture into position * 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 932870448f85b..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 @@ -7,16 +7,16 @@ namespace Magento\TestFramework\Workaround\Override\Fixture; +use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Component\ComponentRegistrarInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Annotation\AdminConfigFixture; use Magento\TestFramework\Annotation\ConfigFixture; use Magento\TestFramework\Annotation\DataFixture; use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Workaround\Override\Config; +use Magento\TestFramework\Workaround\Override\ConfigInterface; use Magento\TestFramework\Workaround\Override\Fixture\Applier\AdminConfigFixture as AdminConfigFixtureApplier; use Magento\TestFramework\Workaround\Override\Fixture\Applier\ApplierInterface; use Magento\TestFramework\Workaround\Override\Fixture\Applier\Base; @@ -29,30 +29,30 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Resolver +class Resolver implements ResolverInterface { + /** @var ObjectManagerInterface */ + protected $objectManager; + /** @var self */ private static $instance; /** @var TestCase */ private $currentTest; - /** @var Config */ + /** @var ConfigInterface */ private $config; /** @var ApplierInterface[] */ private $appliersList; - /** @var ObjectManagerInterface */ - private $objectManager; - /** @var string */ private $currentFixtureType = null; /** - * @param Config $config + * @param ConfigInterface $config */ - public function __construct(Config $config) + public function __construct(ConfigInterface $config) { $this->config = $config; $this->objectManager = Bootstrap::getObjectManager(); @@ -61,32 +61,38 @@ public function __construct(Config $config) /** * Get class instance * - * @return self + * @return ResolverInterface */ - public static function getInstance(): self + public static function getInstance(): ResolverInterface { if (empty(self::$instance)) { - self::$instance = new self(Config::getInstance()); + throw new \RuntimeException('Override fixture resolver isn\'t initialized'); } return self::$instance; } /** - * Set current test to instance + * Instance setter. * - * @param TestCase $currentTest + * @param ResolverInterface $instance * @return void */ + public static function setInstance(ResolverInterface $instance): void + { + self::$instance = $instance; + } + + /** + * @inheritdoc + */ public function setCurrentTest(?TestCase $currentTest): void { $this->currentTest = $currentTest; } /** - * Get current test - * - * @return TestCase|null + * @inheritdoc */ public function getCurrentTest(): ?TestCase { @@ -94,10 +100,7 @@ public function getCurrentTest(): ?TestCase } /** - * Set which fixture type is executed - * - * @param null|string $fixtureType - * @return void + * @inheritdoc */ public function setCurrentFixtureType(?string $fixtureType): void { @@ -105,10 +108,7 @@ public function setCurrentFixtureType(?string $fixtureType): void } /** - * Require fixture wrapper - * - * @param string $path - * @return void + * @inheritdoc */ public function requireDataFixture(string $path): void { @@ -123,38 +123,62 @@ public function requireDataFixture(string $path): void } /** - * Apply override configurations to config fixtures list - * - * @param TestCase $test - * @param array $fixtures - * @param string $fixtureType - * @return array + * @inheritdoc */ 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); } /** - * Apply override configurations to data fixtures list - * - * @param TestCase $test - * @param array $fixtures - * @param string $fixtureType - * @return array + * @inheritdoc */ 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; } + /** + * Get appropriate fixture applier according to fixture type + * + * @param string $fixtureType + * @return ApplierInterface + */ + protected function getApplierByFixtureType(string $fixtureType): ApplierInterface + { + switch ($fixtureType) { + case DataFixture::ANNOTATION: + case DataFixtureBeforeTransaction::ANNOTATION: + $applier = $this->objectManager->get(DataFixtureApplier::class); + break; + case ConfigFixture::ANNOTATION: + $applier = $this->objectManager->get(ConfigFixtureApplier::class); + break; + case AdminConfigFixture::ANNOTATION: + $applier = $this->objectManager->get(AdminConfigFixtureApplier::class); + break; + default: + throw new \InvalidArgumentException(sprintf('Unsupported fixture type %s provided', $fixtureType)); + } + + return $applier; + } + /** * Get ComponentRegistrar object * @@ -179,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( @@ -190,32 +215,6 @@ private function getApplier(TestCase $test, string $fixtureType): ApplierInterfa return $applier; } - /** - * Get appropriate fixture applier according to fixture type - * - * @param string $fixtureType - * @return ApplierInterface - */ - private function getApplierByFixtureType(string $fixtureType): ApplierInterface - { - switch ($fixtureType) { - case DataFixture::ANNOTATION: - case DataFixtureBeforeTransaction::ANNOTATION: - $applier = $this->objectManager->get(DataFixtureApplier::class); - break; - case ConfigFixture::ANNOTATION: - $applier = $this->objectManager->get(ConfigFixtureApplier::class); - break; - case AdminConfigFixture::ANNOTATION: - $applier = $this->objectManager->get(AdminConfigFixtureApplier::class); - break; - default: - throw new \InvalidArgumentException(sprintf('Unsupported fixture type %s provided', $fixtureType)); - } - - return $applier; - } - /** * Converts fixture path. * diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/ResolverInterface.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/ResolverInterface.php new file mode 100644 index 0000000000000..3701ba033802f --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/ResolverInterface.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Workaround\Override\Fixture; + +use PHPUnit\Framework\TestCase; + +/** + * Class determines fixture applying according to configurations + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +interface ResolverInterface +{ + /** + * Set current test to instance + * + * @param TestCase $currentTest + * @return void + */ + public function setCurrentTest(?TestCase $currentTest): void; + + /** + * Get current test + * + * @return TestCase|null + */ + public function getCurrentTest(): ?TestCase; + + /** + * Set which fixture type is executed + * + * @param null|string $fixtureType + * @return void + */ + public function setCurrentFixtureType(?string $fixtureType): void; + + /** + * Require fixture wrapper + * + * @param string $path + * @return void + */ + public function requireDataFixture(string $path): void; + + /** + * Apply override configurations to config fixtures list + * + * @param TestCase $test + * @param array $fixtures + * @param string $fixtureType + * @return array + */ + public function applyConfigFixtures(TestCase $test, array $fixtures, string $fixtureType): array; + + /** + * Apply override configurations to data fixtures list + * + * @param TestCase $test + * @param array $fixtures + * @param string $fixtureType + * @return array + */ + public function applyDataFixtures(TestCase $test, array $fixtures, string $fixtureType): array; +} 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/bootstrap.php b/dev/tests/integration/framework/bootstrap.php index 59fb1535d1884..acf3056c8d923 100644 --- a/dev/tests/integration/framework/bootstrap.php +++ b/dev/tests/integration/framework/bootstrap.php @@ -103,9 +103,16 @@ $themePackageList ) ); - + $overrideConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Magento\TestFramework\Workaround\Override\Config::class + ); + $overrideConfig->init(); + Magento\TestFramework\Workaround\Override\Config::setInstance($overrideConfig); + Magento\TestFramework\Workaround\Override\Fixture\Resolver::setInstance( + new \Magento\TestFramework\Workaround\Override\Fixture\Resolver($overrideConfig) + ); /* Unset declared global variables to release the PHPUnit from maintaining their values between tests */ - unset($testsBaseDir, $logWriter, $settings, $shell, $application, $bootstrap); + unset($testsBaseDir, $settings, $shell, $application, $bootstrap, $overrideConfig); } catch (\Exception $e) { // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $e . PHP_EOL; 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/AdminNotification/_files/notifications.php b/dev/tests/integration/testsuite/Magento/AdminNotification/_files/notifications.php index 6615c24320b21..0a8f2670b5740 100644 --- a/dev/tests/integration/testsuite/Magento/AdminNotification/_files/notifications.php +++ b/dev/tests/integration/testsuite/Magento/AdminNotification/_files/notifications.php @@ -3,52 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$om = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Unread Critical 1' -)->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity(\Magento\Framework\Notification\MessageInterface::SEVERITY_MAJOR) - ->setTitle('Unread Major 1') - ->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Unread Critical 2' -)->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Unread Critical 3' -)->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Read Critical 1' -)->setIsRead( - 1 -)->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity(\Magento\Framework\Notification\MessageInterface::SEVERITY_MAJOR) - ->setTitle('Unread Major 2') - ->save(); - -$message = $om->create(\Magento\AdminNotification\Model\Inbox::class); -$message->setSeverity( - \Magento\Framework\Notification\MessageInterface::SEVERITY_CRITICAL -)->setTitle( - 'Removed Critical 1' -)->setIsRemove( - 1 -)->save(); + +declare(strict_types=1); + +use Magento\AdminNotification\Model\Inbox; +use Magento\AdminNotification\Model\ResourceModel\Inbox as InboxResource; +use Magento\Framework\Notification\MessageInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Inbox $message + * @var InboxResource $messageResource + */ +$message = $objectManager->create(Inbox::class); +$messageResource = $objectManager->create(InboxResource::class); + +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Unread Critical 1'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_MAJOR)->setTitle('Unread Major 1'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Unread Critical 2'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Unread Critical 3'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Read Critical 1')->setIsRead(1); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_MAJOR)->setTitle('Unread Major 2'); +$messageResource->save($message); + +$message = $objectManager->create(Inbox::class); +$message->setSeverity(MessageInterface::SEVERITY_CRITICAL)->setTitle('Removed Critical 1')->setIsRemove(1); +$messageResource->save($message); diff --git a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products.php index ef5877612a3b9..2ae807f0a401b 100644 --- a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products.php +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products.php @@ -4,26 +4,50 @@ * See COPYING.txt for license details. */ -$productModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\Product::class); +declare(strict_types=1); -$productModel->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Product $productModel + * @var ProductRepositoryInterface $productRepository + */ +$productModel = $objectManager->create(Product::class); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); + +$productModel->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId(4) ->setName('AdvancedPricingSimple 1') ->setSku('AdvancedPricingSimple 1') ->setPrice(321) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) ->setWebsiteIds([1]) ->setCategoryIds([]) ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) - ->setIsObjectNew(true) - ->save(); + ->setIsObjectNew(true); -$productModel->setName('AdvancedPricingSimple 2') - ->setId(null) - ->setUrlKey(null) +$productRepository->save($productModel); + +$productModel = $objectManager->create(Product::class); +$productModel->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('AdvancedPricingSimple 2') ->setSku('AdvancedPricingSimple 2') ->setPrice(654) - ->setIsObjectNew(true) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) + ->setIsObjectNew(true); +$productRepository->save($productModel); diff --git a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website.php index 47456de5ab07e..17b6a700e0c07 100644 --- a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website.php +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website.php @@ -4,7 +4,12 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Model\Group; +use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use Magento\Store\Api\WebsiteRepositoryInterface; @@ -13,15 +18,16 @@ Resolver::getInstance()->requireDataFixture('Magento/AdvancedPricingImportExport/_files/create_products.php'); $objectManager = Bootstrap::getObjectManager(); -/** @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager - ->get(Magento\Catalog\Api\ProductAttributeRepositoryInterface::class); -$groupPriceAttribute = $attributeRepository->get('tier_price') - ->setScope(Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_WEBSITE); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +$groupPriceAttribute = $attributeRepository->get('tier_price')->setScope(ScopedAttributeInterface::SCOPE_WEBSITE); $attributeRepository->save($groupPriceAttribute); + /** @var WebsiteRepositoryInterface $websiteRepository */ $websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); $website = $websiteRepository->get('test'); + /** @var ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(ProductRepositoryInterface::class); $productModel = $productRepository->get('AdvancedPricingSimple 2'); @@ -30,10 +36,10 @@ [ [ 'website_id' => $website->getId(), - 'cust_group' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'cust_group' => Group::CUST_GROUP_ALL, 'price_qty' => 3, 'price' => 5 ] ] ); -$productModel->save(); +$productRepository->save($productModel); 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/Controller/Adminhtml/Bundle/Product/Edit/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/MassDeleteTest.php new file mode 100644 index 0000000000000..9343e6201f7ff --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/MassDeleteTest.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; + +use Magento\Catalog\Controller\Adminhtml\Product\MassDeleteTest as CatalogMassDeleteTest; + +/** + * Test for mass bundle product deleting. + * + * @see \Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit\MassDelete + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class MassDeleteTest extends CatalogMassDeleteTest +{ + /** + * @magentoDataFixture Magento/Bundle/_files/bundle_product_checkbox_required_option.php + * + * @return void + */ + public function testDeleteBundleProductViaMassAction(): void + { + $product = $this->productRepository->get('bundle-product-checkbox-required-option'); + $this->dispatchMassDeleteAction([$product->getId()]); + $this->assertSuccessfulDeleteProducts(1); + } +} 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/Controller/Adminhtml/Product/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php new file mode 100644 index 0000000000000..6384883c56c58 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/MassDeleteTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for mass product deleting. + * + * @see \Magento\Catalog\Controller\Adminhtml\Product\MassDelete + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class MassDeleteTest extends AbstractBackendController +{ + /** @var ProductRepositoryInterface */ + protected $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php + * + * @return void + */ + public function testDeleteSimpleProductViaMassAction(): void + { + $productIds = [10, 11, 12]; + $this->dispatchMassDeleteAction($productIds); + $this->assertSuccessfulDeleteProducts(count($productIds)); + } + + /** + * @return void + */ + public function testDeleteNotExistingProductViaMassAction(): void + { + $this->dispatchMassDeleteAction([989]); + $this->assertSessionMessages($this->isEmpty(), MessageInterface::TYPE_ERROR); + $this->assertRedirect($this->stringContains('backend/catalog/product/index')); + } + + /** + * @return void + */ + public function testMassDeleteWithoutProductIds(): void + { + $this->markTestSkipped('Test is blocked by issue MC-34495'); + $this->dispatchMassDeleteAction(); + $this->assertSessionMessages( + $this->equalTo('An item needs to be selected. Select and try again.'), + MessageInterface::TYPE_ERROR + ); + $this->assertRedirect($this->stringContains('backend/catalog/product/index')); + } + + /** + * Assert successful delete products. + * + * @param int $productCount + * @return void + */ + protected function assertSuccessfulDeleteProducts(int $productCount): void + { + $this->assertSessionMessages( + $this->equalTo([(string)__('A total of %1 record(s) have been deleted.', $productCount)]), + MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('backend/catalog/product/index')); + } + + /** + * Dispatch mass delete action. + * + * @param array $productIds + * @return void + */ + protected function dispatchMassDeleteAction(array $productIds = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams(['selected' => $productIds, 'namespace' => 'product_listing']); + $this->dispatch('backend/catalog/product/massDelete/'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php index 460488fdfae76..4f046eccbe59f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php @@ -412,7 +412,7 @@ protected function _assertCompareListEquals(array $expectedProductIds) $compareItems = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection::class ); - $compareItems->useProductItem(true); + $compareItems->useProductItem(); // important $compareItems->setVisitorId( \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompositeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompositeTest.php index a558a99bd2f17..bc310f8bd65b5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompositeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompositeTest.php @@ -3,34 +3,51 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Helper\Product; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * Test Composite */ -class CompositeTest extends \PHPUnit\Framework\TestCase +class CompositeTest extends TestCase { - /** - * @var Composite - */ - protected $helper; + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Composite */ + private $helper; + + /** @var Registry */ + private $registry; + + /** @var ProductRepositoryInterface */ + private $productRepository; /** - * @var Registry + * @inheritdoc */ - protected $registry; - protected function setUp(): void { - $this->helper = Bootstrap::getObjectManager()->get(\Magento\Catalog\Helper\Product\Composite::class); - $this->registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(Composite::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); } + /** + * @inheritdoc + */ protected function tearDown(): void { $this->registry->unregister('composite_configure_result_error_message'); @@ -42,40 +59,85 @@ protected function tearDown(): void /** * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void */ - public function testRenderConfigureResult() + public function testRenderConfigureResult(): void { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); - /** @var $product \Magento\Catalog\Model\Product */ - $product = $productRepository->get('simple'); - - $configureResult = new \Magento\Framework\DataObject(); + $product = $this->productRepository->get('simple'); + /** @var DataObject $buyRequest */ + $buyRequest = $this->objectManager->create(DataObject::class); + $buyRequest->setData(['qty' => 1]); + /** @var DataObject $configureResult */ + $configureResult = $this->objectManager->create(DataObject::class); $configureResult->setOk(true) ->setProductId($product->getId()) + ->setBuyRequest($buyRequest) ->setCurrentCustomerId(1); - $this->helper->renderConfigureResult($configureResult); + $resultLayout = $this->helper->renderConfigureResult($configureResult); - $customerId = $this->registry->registry(RegistryConstants::CURRENT_CUSTOMER_ID); - $this->assertEquals(1, $customerId); - $errorMessage = $this->registry->registry('composite_configure_result_error_message'); - $this->assertNull($errorMessage); + /** @var Product $preparedProduct */ + $preparedProduct = $this->registry->registry('product'); + $preparedCurrentProduct = $this->registry->registry('current_product'); + $this->assertTrue($preparedProduct && $preparedCurrentProduct); + $this->assertEquals(1, $this->registry->registry(RegistryConstants::CURRENT_CUSTOMER_ID)); + $this->assertNotNull($preparedProduct->getPreconfiguredValues()); + $this->assertContains( + 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE', + $resultLayout->getLayout()->getUpdate()->getHandles() + ); + $this->assertContains( + 'catalog_product_view_type_' . $product->getTypeId(), + $resultLayout->getLayout()->getUpdate()->getHandles() + ); } - public function testRenderConfigureResultNotOK() + /** + * @dataProvider renderConfigureResultExceptionProvider + * @param array $data + * @param string $expectedErrorMessage + * @return void + */ + public function testRenderConfigureResultException(array $data, string $expectedErrorMessage): void { - $configureResult = new \Magento\Framework\DataObject(); - $configureResult->setError(true) - ->setMessage('Test Message'); + /** @var DataObject $configureResult */ + $configureResult = $this->objectManager->create(DataObject::class); + $configureResult->setData($data); - $this->helper->renderConfigureResult($configureResult); + $resultLayout = $this->helper->renderConfigureResult($configureResult); + + $this->assertEquals( + $expectedErrorMessage, + $this->registry->registry('composite_configure_result_error_message') + ); + $this->assertContains( + 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE_ERROR', + $resultLayout->getLayout()->getUpdate()->getHandles() + ); + } - $customerId = $this->registry->registry(RegistryConstants::CURRENT_CUSTOMER_ID); - $this->assertNull($customerId); - $errorMessage = $this->registry->registry('composite_configure_result_error_message'); - $this->assertEquals('Test Message', $errorMessage); + /** + * Create render configure result exception provider + * + * @return array + */ + public function renderConfigureResultExceptionProvider(): array + { + return [ + 'error_true' => [ + 'data' => [ + 'error' => true, + 'message' => 'Test Message' + ], + 'expected_error_message' => 'Test Message', + ], + 'without_product' => [ + 'data' => [ + 'ok' => true, + ], + 'expected_error_message' => 'The product that was requested doesn\'t exist.' + . ' Verify the product and try again.', + ], + ]; } } 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/Model/ProductRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php index 0fe3ef55455d2..af7a027367fff 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php @@ -7,15 +7,22 @@ namespace Magento\Catalog\Model; -use Magento\Backend\Model\Auth; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Media\ConfigInterface; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\StateException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Bootstrap as TestBootstrap; -use Magento\Framework\Acl\Builder; +use PHPUnit\Framework\TestCase; /** * Provide tests for ProductRepository model. @@ -24,8 +31,13 @@ * @magentoAppIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ProductRepositoryTest extends \PHPUnit\Framework\TestCase +class ProductRepositoryTest extends TestCase { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** * Test subject. * @@ -54,40 +66,68 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase private $layoutManager; /** - * Sets up common objects + * @var ConfigInterface + */ + private $mediaConfig; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var array + */ + private $productSkusToDelete = []; + + /** + * @inheritdoc */ protected function setUp(): void { - $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); - $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); - $this->productFactory = Bootstrap::getObjectManager()->get(ProductFactory::class); - $this->productResource = Bootstrap::getObjectManager()->get(ProductResource::class); - $this->layoutManager = Bootstrap::getObjectManager()->get(ProductLayoutUpdateManager::class); + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $this->productFactory = $this->objectManager->get(ProductFactory::class); + $this->productResource = $this->objectManager->get(ProductResource::class); + $this->layoutManager = $this->objectManager->get(ProductLayoutUpdateManager::class); + $this->mediaConfig = $this->objectManager->get(ConfigInterface::class); + $this->mediaDirectory = $this->objectManager->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); } /** - * Create new subject instance. - * - * @return ProductRepositoryInterface + * @inheritdoc */ - private function createRepo(): ProductRepositoryInterface + protected function tearDown(): void { - return Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + foreach ($this->productSkusToDelete as $productSku) { + try { + $this->productRepository->deleteById($productSku); + } catch (NoSuchEntityException $e) { + //Product already removed + } + } + + parent::tearDown(); } /** * Checks filtering by store_id * * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/product_simple.php + * @return void */ - public function testFilterByStoreId() + public function testFilterByStoreId(): void { $searchCriteria = $this->searchCriteriaBuilder ->addFilter('store_id', '1', 'eq') ->create(); $list = $this->productRepository->getList($searchCriteria); $count = $list->getTotalCount(); - $this->assertGreaterThanOrEqual(1, $count); } @@ -99,13 +139,11 @@ public function testFilterByStoreId() * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @dataProvider skuDataProvider */ - public function testGetProduct(string $sku) : void + public function testGetProduct(string $sku): void { $expectedSku = 'simple'; $product = $this->productRepository->get($sku); - - self::assertNotEmpty($product); - self::assertEquals($expectedSku, $product->getSku()); + $this->assertEquals($expectedSku, $product->getSku()); } /** @@ -127,45 +165,29 @@ public function skuDataProvider(): array * * @magentoDataFixture Magento/Catalog/_files/product_simple_with_image.php * - * @throws \Magento\Framework\Exception\CouldNotSaveException - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\StateException + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws StateException */ public function testSaveProductWithGalleryImage(): void { - /** @var $mediaConfig \Magento\Catalog\Model\Product\Media\Config */ - $mediaConfig = Bootstrap::getObjectManager() - ->get(\Magento\Catalog\Model\Product\Media\Config::class); - - /** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ - $mediaDirectory = Bootstrap::getObjectManager() - ->get(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); - - $product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); - $product->load(1); - - $path = $mediaConfig->getBaseMediaPath() . '/magento_image.jpg'; - $absolutePath = $mediaDirectory->getAbsolutePath() . $path; + $product = $this->productRepository->get('simple'); + $path = $this->mediaConfig->getBaseMediaPath() . '/magento_image.jpg'; + $absolutePath = $this->mediaDirectory->getAbsolutePath() . $path; $product->addImageToMediaGallery( $absolutePath, [ - 'image', - 'small_image', + 'image', + 'small_image', ], false, false ); - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $productRepository->save($product); - + $this->productRepository->save($product); $gallery = $product->getData('media_gallery'); $this->assertArrayHasKey('images', $gallery); $images = array_values($gallery['images']); - $this->assertNotEmpty($gallery); $this->assertTrue(isset($images[0]['file'])); $this->assertStringStartsWith('/m/a/magento_image', $images[0]['file']); @@ -179,58 +201,70 @@ public function testSaveProductWithGalleryImage(): void * Test Product Repository can change(update) "sku" for given product. * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation enabled * @magentoAppArea adminhtml + * @return void */ - public function testUpdateProductSku() + public function testUpdateProductSku(): void { $newSku = 'simple-edited'; $productId = $this->productResource->getIdBySku('simple'); $initialProduct = $this->productFactory->create(); $this->productResource->load($initialProduct, $productId); - $initialProduct->setSku($newSku); $this->productRepository->save($initialProduct); - + $this->productSkusToDelete[] = $newSku; $updatedProduct = $this->productFactory->create(); $this->productResource->load($updatedProduct, $productId); - self::assertSame($newSku, $updatedProduct->getSku()); - - //clean up. - $this->productRepository->delete($updatedProduct); + $this->assertSame($newSku, $updatedProduct->getSku()); } /** * Test that custom layout file attribute is saved. * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @return void * @throws \Throwable - * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled */ public function testCustomLayout(): void { - //New valid value - $repo = $this->createRepo(); - $product = $repo->get('simple'); + $product = $this->productRepository->get('simple'); $newFile = 'test'; $this->layoutManager->setFakeFiles((int)$product->getId(), [$newFile]); $product->setCustomAttribute('custom_layout_update_file', $newFile); - $repo->save($product); - $repo = $this->createRepo(); - $product = $repo->get('simple'); + $this->productRepository->save($product); + $product = $this->productRepository->get('simple'); $this->assertEquals($newFile, $product->getCustomAttribute('custom_layout_update_file')->getValue()); - - //Setting non-existent value $newFile = 'does not exist'; $product->setCustomAttribute('custom_layout_update_file', $newFile); - $caughtException = false; - try { - $repo->save($product); - } catch (LocalizedException $exception) { - $caughtException = true; - } - $this->assertTrue($caughtException); + $this->expectException(LocalizedException::class); + $this->productRepository->save($product); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoAppArea adminhtml + * + * @return void + */ + public function testDeleteByIdSimpleProduct(): void + { + $productSku = 'simple-1'; + $result = $this->productRepository->deleteById($productSku); + $this->assertTrue($result); + $this->assertProductNotExist($productSku); + } + + /** + * Assert that product does not exist. + * + * @param string $sku + * @return void + */ + private function assertProductNotExist(string $sku): void + { + $this->expectExceptionObject(new NoSuchEntityException( + __("The product that was requested doesn't exist. Verify the product and try again.") + )); + $this->productRepository->get($sku); } } 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_boolean_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php index 34dccc2284445..57b918fb5e663 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php @@ -9,6 +9,7 @@ use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Eav\Model\Entity\Attribute\Source\Boolean; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); @@ -19,32 +20,36 @@ /** @var $installer CategorySetup */ $installer = $objectManager->create(CategorySetup::class); -$attribute->setData( - [ - 'attribute_code' => 'boolean_attribute', - 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, - 'is_global' => 0, - 'is_user_defined' => 1, - 'frontend_input' => 'boolean', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 0, - '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' => 0, - 'frontend_label' => ['Boolean Attribute'], - 'backend_type' => 'int', - 'source_model' => Boolean::class - ] -); +try { + $attributeRepository->get(CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, 'boolean_attribute'); +} catch (NoSuchEntityException $e) { + $attribute->setData( + [ + 'attribute_code' => 'boolean_attribute', + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'boolean', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + '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' => 0, + 'frontend_label' => ['Boolean Attribute'], + 'backend_type' => 'int', + 'source_model' => Boolean::class + ] + ); -$attributeRepository->save($attribute); + $attributeRepository->save($attribute); -/* Assign attribute to attribute set */ -$installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); +} 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/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php index 3d02b2a469e29..d87a7ffd48c09 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php @@ -15,6 +15,7 @@ use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; +use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\Store\Model\ScopeInterface; use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; use Magento\UrlRewrite\Model\OptionProvider; @@ -63,6 +64,8 @@ protected function setUp(): void } /** + * Test url rewrite after category save + * * @magentoDataFixture Magento/Catalog/_files/category_with_position.php * @dataProvider categoryProvider * @param array $data @@ -80,6 +83,8 @@ public function testUrlRewriteOnCategorySave(array $data): void } /** + * Provider. categoryProvider + * * @return array */ public function categoryProvider(): array @@ -123,6 +128,8 @@ public function categoryProvider(): array } /** + * Test category product url rewrite + * * @magentoDataFixture Magento/Catalog/_files/category_tree.php * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php * @dataProvider productRewriteProvider @@ -140,6 +147,8 @@ public function testCategoryProductUrlRewrite(array $data): void } /** + * Provider. productRewriteProvider + * * @return array */ public function productRewriteProvider(): array @@ -165,6 +174,8 @@ public function productRewriteProvider(): array } /** + * Test url rewrites after category save with existing url key + * * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_products.php * @magentoAppIsolation enabled * @dataProvider existingUrlProvider @@ -179,6 +190,8 @@ public function testUrlRewriteOnCategorySaveWithExistingUrlKey(array $data): voi } /** + * Provider. existingUrlProvider + * * @return array */ public function existingUrlProvider(): array @@ -226,6 +239,8 @@ public function existingUrlProvider(): array } /** + * Test url rewrites after category move + * * @magentoDataFixture Magento/Catalog/_files/category_product.php * @magentoDataFixture Magento/Catalog/_files/catalog_category_with_slash.php * @dataProvider categoryMoveProvider @@ -287,6 +302,7 @@ public function categoryMoveProvider(): array } /** + * Test url rewrites after category delete * @magentoDataFixture Magento/Catalog/_files/category.php * @return void */ @@ -302,6 +318,8 @@ public function testUrlRewritesAfterCategoryDelete(): void } /** + * Test url rewrites after category with products delete + * * @magentoAppArea adminhtml * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_product_ids.php * @return void @@ -325,6 +343,8 @@ public function testUrlRewritesAfterCategoryWithProductsDelete(): void } /** + * Test category url rewrite per Store Views + * * @magentoDataFixture Magento/Store/_files/second_store.php * @magentoDataFixture Magento/Catalog/_files/category.php * @return void @@ -353,6 +373,53 @@ public function testCategoryUrlRewritePerStoreViews(): void } } + /** + * Test category url rewrite while reassign store view + * + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Store/_files/second_store_group_with_second_website.php + * @magentoDataFixture Magento/Catalog/_files/category.php + * @return void + */ + public function testCategoryUrlRewriteMovingToOtherStoreView(): void + { + $categoryId = 333; + $store = $this->storeRepository->get('default'); + $storeId = $store->getId(); + $urlRewrites = [ + ['category-1-updated.html', 'category-1.html'], + ['category-1-most-recent.html', 'category-1-updated.html'], + ]; + foreach ($urlRewrites as $rewrite) { + /** @var \Magento\UrlRewrite\Model\UrlRewrite $urlRewrite */ + $urlRewrite = $this->objectManager->create(\Magento\UrlRewrite\Model\UrlRewrite::class); + $urlRewrite->setEntityType(\Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($categoryId) + ->setRequestPath($rewrite[0]) + ->setTargetPath($rewrite[1]) + ->setRedirectType(\Magento\UrlRewrite\Model\OptionProvider::PERMANENT) + ->setStoreId($storeId); + $urlRewrite->save(); + } + + /** @var WebsiteRepositoryInterface $websiteRepo */ + $websiteRepo = $this->objectManager->get(WebsiteRepositoryInterface::class); + $website = $websiteRepo->get('test'); + $group = $website->getDefaultGroup(); + $group->setRootCategoryId(2); + $group->save(); + $groupId = $group->getId(); + $store->setStoreGroupId($groupId); + $store->save(); + + $urlRewriteItems = $this->getEntityRewriteCollection($categoryId)->getItems(); + $this->assertTrue(count($urlRewriteItems) === 3); + $expectedRewriteRequestPaths = ['category-1.html', 'category-1-updated.html', 'category-1-most-recent.html']; + foreach ($urlRewriteItems as $item) { + $this->assertTrue(in_array($item->getRequestPath(), $expectedRewriteRequestPaths)); + } + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php new file mode 100644 index 0000000000000..66d984301d14f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile; +use Magento\Catalog\Model\Product\Option\Value; +use Magento\Checkout\_files\ValidatorFileMock; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\DataObject; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_with_options.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_uk_address.php'); + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var Quote $quote */ +$quote = $objectManager->get(QuoteFactory::class)->create(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); + +$customer = $customerRepository->get('customer_uk_address@test.com'); +$product = $productRepository->get('simple'); +$options = []; +$dropDownValues = []; +$iDate = 1; +/** @var Option $option */ +foreach ($product->getOptions() as $option) { + switch ($option->getGroupByType()) { + case ProductCustomOptionInterface::OPTION_GROUP_SELECT: + if ($option->getType() == ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { + $dropDownValues = $option->getValues(); + $value = null; + } elseif ($option->getType() == ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX) { + $value = array_keys($option->getValues()); + } else { + $value = (string)key($option->getValues()); + } + break; + case ProductCustomOptionInterface::OPTION_GROUP_DATE: + $value = [ + 'year' => 2013 + $iDate, + 'month' => 1 + $iDate, + 'day' => 1 + $iDate, + 'hour' => 10 + $iDate, + 'minute' => 30 + $iDate, + ]; + $iDate++; + break; + case ProductCustomOptionInterface::OPTION_GROUP_FILE: + $value = 'test.jpg'; + break; + default: + $value = 'test'; + break; + } + $options[$option->getId()] = $value; +} + +$itemsOptions = []; +/** @var Value $dropDownValue */ +foreach ($dropDownValues as $dropDownId => $dropDownValue) { + $options[$dropDownValue->getOption()->getId()] = $dropDownId; + $itemsOptions[$dropDownValue->getTitle()] = $options; +} + +$validatorFileMock = (new ValidatorFileMock())->getInstance(); +$objectManager->addSharedInstance($validatorFileMock, ValidatorFile::class); + +$quote->setStoreId($storeManager->getStore()->getId()) + ->assignCustomer($customer) + ->setReservedOrderId('customer_quote_product_custom_options'); + +/** @var DataObject $request */ +$requestInfo = $objectManager->create(DataObject::class); + +foreach ($itemsOptions as $itemOptions) { + $requestInfo->setData(['qty' => 1, 'options' => $itemOptions]); + $product = clone $product; + $quote->addProduct($product, $requestInfo); +} + +$quoteRepository->save($quote); +$objectManager->removeSharedInstance(ValidatorFile::class); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options_rollback.php new file mode 100644 index 0000000000000..5877e9a5ef975 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('customer_quote_product_custom_options'); +if ($quote !== null) { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $objectManager->get(CartRepositoryInterface::class); + $quoteRepository->delete($quote); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_uk_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_with_options_rollback.php'); 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/Cms/_files/block.php b/dev/tests/integration/testsuite/Magento/Cms/_files/block.php index 070fd9ae2a0b3..4625c1fe3313b 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/block.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/block.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Model\Block; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $block Block + * @var $blockRepository BlockRepositoryInterface + */ +$block = $objectManager->create(Block::class); +$blockRepository = $objectManager->create(BlockRepositoryInterface::class); -/** @var $block \Magento\Cms\Model\Block */ -$block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Block::class); $block->setTitle( 'CMS Block Title' )->setIdentifier( @@ -20,8 +33,10 @@ 1 )->setStores( [ - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class + Bootstrap::getObjectManager()->get( + StoreManagerInterface::class )->getStore()->getId() ] -)->save(); +); + +$blockRepository->save($block); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/block_default_store.php b/dev/tests/integration/testsuite/Magento/Cms/_files/block_default_store.php index de4e852f807bc..825103d76ecff 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/block_default_store.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/block_default_store.php @@ -5,12 +5,20 @@ */ declare(strict_types=1); +use Magento\Cms\Api\BlockRepositoryInterface; use Magento\Cms\Model\Block; use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; -/** @var $block Block */ -$block = Bootstrap::getObjectManager()->create(Block::class); +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $block Block + * @var $blockRepository BlockRepositoryInterface + */ +$block = $objectManager->create(Block::class); +$blockRepository = $objectManager->create(BlockRepositoryInterface::class); + $block->setTitle( 'CMS Block Title' )->setIdentifier( @@ -24,4 +32,6 @@ 1 )->setStores( [Store::DEFAULT_STORE_ID] -)->save(); +); + +$blockRepository->save($block); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/home_with_custom_handle.php b/dev/tests/integration/testsuite/Magento/Cms/_files/home_with_custom_handle.php index 2556e0318222d..a4dd0c5fd4e56 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/home_with_custom_handle.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/home_with_custom_handle.php @@ -5,6 +5,7 @@ */ declare(strict_types=1); +use Magento\Cms\Model\ResourceModel\Page as PageResource; use Magento\Cms\Model\Page as PageModel; use Magento\Cms\Model\PageFactory as PageModelFactory; use Magento\TestFramework\Cms\Model\CustomLayoutManager; @@ -20,11 +21,16 @@ $customLayoutName = 'page_custom_layout'; -/** @var PageModel $page */ +/** + * @var PageModel $page + * @var PageResource $pageResource + */ $page = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); -$page->load('home'); +$pageResource = $objectManager->create(PageResource::class); + +$pageResource->load($page, 'home'); $cmsPageId = (int)$page->getId(); $fakeManager->fakeAvailableFiles($cmsPageId, [$customLayoutName]); $page->setData('layout_update_selected', $customLayoutName); -$page->save(); +$pageResource->save($page); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/noroute.php b/dev/tests/integration/testsuite/Magento/Cms/_files/noroute.php index 4c56132a12c01..6fb93a266036c 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/noroute.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/noroute.php @@ -3,6 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Page::class); -$block->load('no-route', 'identifier'); -$block->setIsActive(0)->save(); + +declare(strict_types=1); + +use Magento\Cms\Model\Page; +use Magento\Cms\Model\ResourceModel\Page as PageResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Page $page + * @var PageResource $pageResource + */ +$page = $objectManager->create(Page::class); +$pageResource = $objectManager->create(PageResource::class); + +$pageResource->load($page, 'no-route', 'identifier'); +$page->setIsActive(0); +$pageResource->save($page); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php index b2742ecd380f3..3581fdc34f8e5 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php @@ -4,8 +4,21 @@ * See COPYING.txt for license details. */ -/** @var $page \Magento\Cms\Model\Page */ -$page = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Page::class); +declare(strict_types=1); + +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $page Page + * @var $pageRepository PageRepositoryInterface + */ +$page = $objectManager->create(Page::class); +$pageRepository = $objectManager->create(PageRepositoryInterface::class); + $page->setTitle('Cms Page 100') ->setIdentifier('page100') ->setStores([0]) @@ -15,10 +28,10 @@ ->setMetaTitle('Cms Meta title for page100') ->setMetaKeywords('Cms Meta Keywords for page100') ->setMetaDescription('Cms Meta Description for page100') - ->setPageLayout('1column') - ->save(); + ->setPageLayout('1column'); +$pageRepository->save($page); -$page = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Page::class); +$page = $objectManager->create(Page::class); $page->setTitle('Cms Page Design Blank') ->setIdentifier('page_design_blank') ->setStores([0]) @@ -29,5 +42,5 @@ ->setMetaKeywords('Cms Meta Keywords for Blank page') ->setMetaDescription('Cms Meta Description for Blank page') ->setPageLayout('1column') - ->setCustomTheme('Magento/blank') - ->save(); + ->setCustomTheme('Magento/blank'); +$pageRepository->save($page); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php index 9734ed3abaeed..c7ea5f6380b32 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php @@ -7,16 +7,21 @@ declare(strict_types=1); use Magento\Cms\Model\Page as PageModel; +use Magento\Cms\Model\ResourceModel\Page as PageResource; use Magento\Cms\Model\PageFactory as PageModelFactory; use Magento\TestFramework\Cms\Model\CustomLayoutManager; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); $pageFactory = $objectManager->get(PageModelFactory::class); + /** @var CustomLayoutManager $fakeManager */ $fakeManager = $objectManager->get(CustomLayoutManager::class); $layoutRepo = $objectManager->create(PageModel\CustomLayoutRepositoryInterface::class, ['manager' => $fakeManager]); +/** @var PageResource $pageRepository */ +$pageResource = $objectManager->create(PageResource::class); + /** @var PageModel $page */ $page = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); $page->setIdentifier('test_custom_layout_page_1'); @@ -25,14 +30,16 @@ $page->setLayoutUpdateXml('<container />'); $page->setIsActive(true); $page->setStoreId(0); -$page->save(); +$pageResource->save($page); + /** @var PageModel $page2 */ $page2 = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); $page2->setIdentifier('test_custom_layout_page_2'); $page2->setTitle('Test Page 2'); $page->setIsActive(true); $page->setStoreId(0); -$page2->save(); +$pageResource->save($page2); + /** @var PageModel $page3 */ $page3 = $pageFactory->create(['customLayoutRepository' => $layoutRepo]); $page3->setIdentifier('test_custom_layout_page_3'); @@ -41,7 +48,7 @@ $page3->setIsActive(1); $page3->setContent('<h1>Test Page</h1>'); $page3->setPageLayout('1column'); -$page3->save(); +$pageResource->save($page3); $fakeManager->fakeAvailableFiles((int)$page3->getId(), ['test_selected']); $page3->setData('layout_update_selected', 'test_selected'); -$page3->save(); +$pageResource->save($page3); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php index 3217b94d7392b..684b1d4356d20 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml_rollback.php @@ -8,25 +8,33 @@ use Magento\Cms\Model\Page as PageModel; use Magento\Cms\Model\PageFactory as PageModelFactory; +use Magento\Cms\Model\ResourceModel\Page as PageResource; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); $pageFactory = $objectManager->get(PageModelFactory::class); -/** @var PageModel $page */ + +/** + * @var PageModel $page + * @var PageResource $pageResource + */ $page = $pageFactory->create(); -$page->load('test_custom_layout_page_1', PageModel::IDENTIFIER); +$pageResource = $objectManager->create(PageResource::class); +$pageResource->load($page, 'test_custom_layout_page_1', PageModel::IDENTIFIER); if ($page->getId()) { - $page->delete(); + $pageResource->delete($page); } + /** @var PageModel $page2 */ $page2 = $pageFactory->create(); -$page2->load('test_custom_layout_page_2', PageModel::IDENTIFIER); +$pageResource->load($page2, 'test_custom_layout_page_2', PageModel::IDENTIFIER); if ($page2->getId()) { - $page2->delete(); + $pageResource->delete($page2); } + /** @var PageModel $page3 */ $page3 = $pageFactory->create(); -$page3->load('test_custom_layout_page_3', PageModel::IDENTIFIER); +$pageResource->load($page3, 'test_custom_layout_page_3', PageModel::IDENTIFIER); if ($page3->getId()) { - $page3->delete(); + $pageResource->delete($page3); } diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php b/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php index 16e4a4e521fa3..fdb042fbb18fa 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php @@ -5,29 +5,37 @@ */ declare(strict_types=1); +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page; use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store.php'); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$objectManager = Bootstrap::getObjectManager(); + /** @var StoreRepositoryInterface $storeRepository */ $storeRepository = $objectManager->get(StoreRepositoryInterface::class); $store = $storeRepository->get('fixture_second_store'); -/** @var $page \Magento\Cms\Model\Page */ -$page = $objectManager->create(\Magento\Cms\Model\Page::class); + +/** @var PageRepositoryInterface $pageRepository */ +$pageRepository = $objectManager->create(PageRepositoryInterface::class); + +/** @var $page Page */ +$page = $objectManager->create(Page::class); $page->setTitle('First test page') ->setIdentifier('page1') ->setStores([1]) ->setIsActive(1) - ->setPageLayout('1column') - ->save(); + ->setPageLayout('1column'); +$pageRepository->save($page); -/** @var $page \Magento\Cms\Model\Page */ -$page = $objectManager->create(\Magento\Cms\Model\Page::class); +/** @var $page Page */ +$page = $objectManager->create(Page::class); $page->setTitle('Second test page') ->setIdentifier('page1') ->setStores([$store->getId()]) ->setIsActive(1) - ->setPageLayout('1column') - ->save(); + ->setPageLayout('1column'); +$pageRepository->save($page); 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 a5934dd98e2a6..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 @@ -7,19 +7,30 @@ namespace Magento\CmsUrlRewrite\Plugin\Cms\Model\Store; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator; +use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; +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) */ -class ViewTest extends \PHPUnit\Framework\TestCase +class ViewTest extends TestCase { /** * @var UrlFinderInterface @@ -36,6 +47,26 @@ class ViewTest extends \PHPUnit\Framework\TestCase */ private $storeFactory; + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var UrlRewriteFactory + */ + private $urlRewriteFactory; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var CmsPageUrlPathGenerator + */ + private $cmsPageUrlPathGenerator; + /** * @inheritdoc */ @@ -44,31 +75,40 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->urlFinder = $this->objectManager->create(UrlFinderInterface::class); $this->storeFactory = $this->objectManager->create(StoreFactory::class); + $this->urlPersist = $this->objectManager->create(UrlPersistInterface::class); + $this->urlRewriteFactory = $this->objectManager->create(UrlRewriteFactory::class); + $this->pageRepository = $this->objectManager->create(PageRepositoryInterface::class); + $this->cmsPageUrlPathGenerator = $this->objectManager->create(CmsPageUrlPathGenerator::class); } /** * 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, 1); + $this->assertUrlRewritesCount($storeId, 'page100', 1); + $this->editUrlRewrite($storeId, 'page100'); + $this->saveStore($storeId); + $this->assertUrlRewritesCount($storeId, 'page100-test', 1); $this->deleteStore($storeId); - $this->assertUrlRewritesCount($storeId, 0); + $this->assertUrlRewritesCount($storeId, 'page100', 0); } /** - * Assert url rewrites count by store id + * Assert url rewrites count by store id and request path * * @param int $storeId + * @param string $requestPath * @param int $expectedCount */ - private function assertUrlRewritesCount(int $storeId, int $expectedCount): void + private function assertUrlRewritesCount(int $storeId, string $requestPath, int $expectedCount): void { $data = [ - UrlRewrite::REQUEST_PATH => 'page100', + UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::STORE_ID => $storeId ]; $urlRewrites = $this->urlFinder->findAllByData($data); @@ -77,8 +117,6 @@ private function assertUrlRewritesCount(int $storeId, int $expectedCount): void /** * Create test store - * - * @return int */ private function createStore(): int { @@ -95,7 +133,6 @@ private function createStore(): int * Delete test store * * @param int $storeId - * @return void */ private function deleteStore(int $storeId): void { @@ -105,4 +142,49 @@ private function deleteStore(int $storeId): void $store->delete(); } } + + /** + * Edit url rewrite + * + * @param int $storeId + * @param string $pageIdentifier + */ + private function editUrlRewrite(int $storeId, string $pageIdentifier): void + { + $filter = $this->objectManager->create(Filter::class); + $filter->setField('identifier')->setValue($pageIdentifier); + $filterGroup = $this->objectManager->create(FilterGroup::class); + $filterGroup->setFilters([$filter]); + $searchCriteria = $this->objectManager->create(SearchCriteriaInterface::class); + $searchCriteria->setFilterGroups([$filterGroup]); + $pageSearchResults = $this->pageRepository->getList($searchCriteria); + $pages = $pageSearchResults->getItems(); + /** @var PageInterface $page */ + $cmsPage = array_values($pages)[0]; + + $urlRewrite = $this->urlRewriteFactory->create()->setStoreId($storeId) + ->setEntityType(CmsPageUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($cmsPage->getId()) + ->setRequestPath($cmsPage->getIdentifier() . '-test') + ->setTargetPath($this->cmsPageUrlPathGenerator->getCanonicalUrlPath($cmsPage)) + ->setIsAutogenerated(0) + ->setRedirectType(0); + + $this->urlPersist->replace([$urlRewrite]); + } + + /** + * Edit test store + * + * @param int $storeId + * @return void + */ + private function saveStore(int $storeId): void + { + $store = $this->storeFactory->create(); + $store->load($storeId); + if ($store !== null) { + $store->save(); + } + } } 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/Controller/Adminhtml/Product/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/MassDeleteTest.php new file mode 100644 index 0000000000000..4f003e26db43f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/MassDeleteTest.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product; + +use Magento\Catalog\Controller\Adminhtml\Product\MassDeleteTest as CatalogMassDeleteTest; + +/** + * Test for mass configurable product deleting. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class MassDeleteTest extends CatalogMassDeleteTest +{ + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_one_simple.php + * + * @return void + */ + public function testDeleteConfigurableProductViaMassAction(): void + { + $product = $this->productRepository->get('configurable'); + $this->dispatchMassDeleteAction([$product->getId()]); + $this->assertSuccessfulDeleteProducts(1); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php deleted file mode 100644 index 1fffd701c509f..0000000000000 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Interception\PluginList; -use PHPUnit\Framework\TestCase; - -/** - * Test configurable fronted product plugin will add children products ids to configurable product identities. - */ -class ProductIdentitiesExtenderTest extends TestCase -{ - /** - * Check, product identities extender plugin is registered for storefront. - * - * @magentoAppArea frontend - * @return void - */ - public function testIdentitiesExtenderIsRegistered(): void - { - $pluginInfo = Bootstrap::getObjectManager()->get(PluginList::class) - ->get(\Magento\Catalog\Model\Product::class, []); - $this->assertSame(ProductIdentitiesExtender::class, $pluginInfo['product_identities_extender']['instance']); - } - - /** - * Check plugin will add children ids to configurable product identities on storefront. - * - * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - * @magentoAppArea frontend - * @return void - */ - public function testGetIdentitiesForConfigurableProductOnStorefront(): void - { - $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - $configurableProduct = $productRepository->get('configurable'); - $simpleProduct1 = $productRepository->get('simple_10'); - $simpleProduct2 = $productRepository->get('simple_20'); - $expectedIdentities = [ - 'cat_p_' . $configurableProduct->getId(), - 'cat_p', - 'cat_p_' . $simpleProduct1->getId(), - 'cat_p_' . $simpleProduct2->getId(), - - ]; - $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); - } - - /** - * Check plugin won't add children ids to configurable product identities in admin area. - * - * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - * @magentoAppArea adminhtml - * @return void - */ - public function testGetIdentitiesForConfigurableProductInAdminArea(): void - { - $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - $configurableProduct = $productRepository->get('configurable'); - $expectedIdentities = [ - 'cat_p_' . $configurableProduct->getId(), - ]; - $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); - } -} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ProductTest.php index 223c9fbe708e2..c59818aca5191 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ProductTest.php @@ -13,16 +13,37 @@ class ProductTest extends \PHPUnit\Framework\TestCase { /** + * Check that no children identities are added to the parent product in frontend area + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea frontend + * @return void */ - public function testGetIdentities() + public function testGetIdentitiesForConfigurableProductOnStorefront(): void { $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - $confProduct = $productRepository->get('configurable'); - $simple10Product = $productRepository->get('simple_10'); - $simple20Product = $productRepository->get('simple_20'); + $configurableProduct = $productRepository->get('configurable'); + $expectedIdentities = [ + 'cat_p_' . $configurableProduct->getId(), + 'cat_p' + ]; + $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); + } - $this->assertEmpty(array_diff($confProduct->getIdentities(), $simple10Product->getIdentities())); - $this->assertEmpty(array_diff($confProduct->getIdentities(), $simple20Product->getIdentities())); + /** + * Check that no children identities are added to the parent product in frontend area + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + * @return void + */ + public function testGetIdentitiesForConfigurableProductInAdminArea(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $configurableProduct = $productRepository->get('configurable'); + $expectedIdentities = [ + 'cat_p_' . $configurableProduct->getId(), + ]; + $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php new file mode 100644 index 0000000000000..30a9a47f910d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php @@ -0,0 +1,69 @@ +<?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\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as ProductAttribute; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\DataObject; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_uk_address.php'); + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var Quote $quote */ +$quote = $objectManager->get(QuoteFactory::class)->create(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttribute $attribute */ +$attribute = $eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'test_configurable'); + +$customer = $customerRepository->get('customer_uk_address@test.com'); +$quote->setStoreId($storeManager->getStore()->getId()) + ->setIsActive(true) + ->setIsMultiShipping(false) + ->setReservedOrderId('customer_quote_configurable_products') + ->assignCustomer($customer); + +$attributeOptions = $attribute->getOptions(); +unset($attributeOptions[0]); +$productConfigurable = $productRepository->get('configurable'); +/** @var DataObject $request */ +$request = $objectManager->create(DataObject::class); + +foreach ($attributeOptions as $attributeOption) { + $productConfigurable = clone $productConfigurable; + $request->setData( + [ + 'product_id' => $productConfigurable->getId(), + 'super_attribute' => [ + $attribute->getAttributeId() => $attributeOption->getValue() + ], + 'qty' => 1 + ] + ); + $quote->addProduct($productConfigurable, $request); +} +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product_rollback.php new file mode 100644 index 0000000000000..e5cd7637c6e55 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +/** @var $objectManager ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('customer_quote_configurable_products'); +if ($quote !== null) { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $objectManager->get(CartRepositoryInterface::class); + $quoteRepository->delete($quote); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_uk_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products_rollback.php'); 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/Account/AuthenticationPopupTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/AuthenticationPopupTest.php new file mode 100644 index 0000000000000..5744e5e05f5fc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/AuthenticationPopupTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Account; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for authentication popup block. + * + * @see \Magento\Customer\Block\Account\AuthenticationPopup + * @magentoAppArea frontend + */ +class AuthenticationPopupTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var AuthenticationPopup */ + private $block; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(AuthenticationPopup::class); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 1 + * + * @return void + */ + public function testAutocompletePasswordEnabled(): void + { + $this->assertEquals('on', $this->block->getConfig()['autocomplete']); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 0 + * + * @return void + */ + public function testAutocompletePasswordDisabled(): void + { + $this->assertEquals('off', $this->block->getConfig()['autocomplete']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Account/ResetPasswordTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/ResetPasswordTest.php index 80d77a3f90b1c..36de4386a7938 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Account/ResetPasswordTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Account/ResetPasswordTest.php @@ -83,4 +83,24 @@ public function testResetPasswordForm(): void 'Set password button was not found on the page' ); } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 1 + * + * @return void + */ + public function testAutocompletePasswordEnabled(): void + { + $this->assertFalse($this->block->isAutocompleteDisabled()); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 0 + * + * @return void + */ + public function testAutocompletePasswordDisabled(): void + { + $this->assertTrue($this->block->isAutocompleteDisabled()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/AbstractCartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/AbstractCartTest.php new file mode 100644 index 0000000000000..3681da9a10396 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/AbstractCartTest.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\QuoteFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Base class for testing \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart block + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +abstract class AbstractCartTest extends TestCase +{ + const CUSTOMER_ID_VALUE = 1234; + + /** @var Registry */ + private $registry; + + /** @var Cart */ + protected $block; + + /** @var ObjectManagerInterface */ + protected $objectManager; + + /** @var CartRepositoryInterface */ + protected $quoteRepository; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var QuoteFactory */ + private $quoteFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->registerCustomerId(self::CUSTOMER_ID_VALUE); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Cart::class); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->quoteFactory = $this->objectManager->get(QuoteFactory::class); + } + + /** + * @inheritdoc + */ + public function tearDown(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + } + + /** + * Check that the expected items of the shopping cart are in the block + * + * @param string $customerEmail + * @return void + */ + protected function processCheckQuoteItems(string $customerEmail): void + { + $customer = $this->customerRepository->get($customerEmail); + $this->registerCustomerId((int)$customer->getId()); + $this->block->toHtml(); + + $quoteItemIds = $this->getQuoteItemIds((int)$customer->getId()); + $this->assertCount( + count($quoteItemIds), + $this->block->getPreparedCollection(), + "Item's count in the customer cart grid block doesn't match expected count." + ); + $this->assertEmpty( + array_diff( + $this->block->getPreparedCollection()->getAllIds(), + $quoteItemIds + ), + "Items in the customer cart grid block doesn't match expected items." + ); + } + + /** + * Checks that customer's shopping cart block is empty + * + * @param string $customerEmail + * @return void + */ + protected function processCheckWithoutQuoteItems(string $customerEmail): void + { + $customer = $this->customerRepository->get($customerEmail); + $this->registerCustomerId((int)$customer->getId()); + $this->block->toHtml(); + + $this->assertCount( + 0, + $this->block->getPreparedCollection(), + "Item's count in the customer cart grid block doesn't match expected count." + ); + } + + /** + * Add customer id to registry. + * + * @param int $customerId + * @return void + */ + private function registerCustomerId(int $customerId): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->registry->register(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); + } + + /** + * Get shopping cart quote item identifiers by customer id. + * + * @param int $customerId + * @return array + */ + private function getQuoteItemIds(int $customerId): array + { + $ids = []; + /** @var Quote $quote */ + $quote = $this->quoteRepository->getForCustomer($customerId); + /** @var Item $item */ + foreach ($quote->getItems() as $item) { + $ids[] = $item->getId(); + } + + return $ids; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartBundleTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartBundleTest.php new file mode 100644 index 0000000000000..22cb852c73f63 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartBundleTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; + +use Magento\Customer\Block\Adminhtml\Edit\Tab\AbstractCartTest; +use Magento\Framework\Module\Manager; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks customer's shopping cart block with bundle product. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart + * @magentoAppArea adminhtml + */ +class CartBundleTest extends AbstractCartTest +{ + /** @var CollectionFactory */ + private $quoteCollectionFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->quoteCollectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_Bundle + if (!$moduleManager->isEnabled('Magento_Bundle')) { + self::markTestSkipped('Magento_Bundle module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/Bundle/_files/quote_with_bundle_and_options.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void + */ + public function testBundleProductView(): void + { + $quoteCollection = $this->quoteCollectionFactory->create(); + $quoteCollection->addFieldToFilter('reserved_order_id', 'test_cart_with_bundle_and_options'); + /** @var Quote $quote */ + $quote = $quoteCollection->getFirstItem(); + $this->assertNotEmpty($quote->getId()); + $quote->setCustomerId(1); + $this->quoteRepository->save($quote); + $this->processCheckQuoteItems('customer@example.com'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartConfigurableTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartConfigurableTest.php new file mode 100644 index 0000000000000..613edc5aec4f9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CartConfigurableTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; + +use Magento\Customer\Block\Adminhtml\Edit\Tab\AbstractCartTest; +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks customer's shopping cart block with configurable product. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart + * @magentoAppArea adminhtml + */ +class CartConfigurableTest extends AbstractCartTest +{ + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_ConfigurableProduct + if (!$moduleManager->isEnabled('Magento_ConfigurableProduct')) { + self::markTestSkipped('Magento_ConfigurableProduct module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php + * @return void + */ + public function testConfigurableProductView(): void + { + $this->processCheckQuoteItems('customer_uk_address@test.com'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/CartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/CartTest.php index b5abf1de5732b..df799a0878b59 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/CartTest.php @@ -3,82 +3,39 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Block\Adminhtml\Edit\Tab; -use Magento\Backend\Block\Template\Context; use Magento\Backend\Model\Session\Quote as SessionQuote; -use Magento\Customer\Controller\RegistryConstants; -use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\Registry; use Magento\Quote\Model\Quote; -use Magento\Store\Model\StoreManagerInterface; /** - * Magento\Customer\Block\Adminhtml\Edit\Tab\Cart + * Class checks customer's shopping cart block with simple product and simple product with options. * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart * @magentoAppArea adminhtml - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CartTest extends \PHPUnit\Framework\TestCase +class CartTest extends AbstractCartTest { - const CUSTOMER_ID_VALUE = 1234; - - /** - * @var Context - */ - private $_context; - - /** - * @var Registry - */ - private $_coreRegistry; - - /** - * @var StoreManagerInterface - */ - private $_storeManager; - - /** - * @var Cart - */ - private $_block; - - /** - * @var ObjectManagerInterface - */ - private $_objectManager; - /** - * @inheritdoc + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * + * @return void */ - protected function setUp(): void + public function testProductOptionsView(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $this->_storeManager = $this->_objectManager->get(\Magento\Store\Model\StoreManager::class); - $this->_context = $this->_objectManager->get( - \Magento\Backend\Block\Template\Context::class, - ['storeManager' => $this->_storeManager] - ); - - $this->_coreRegistry = $this->_objectManager->get(\Magento\Framework\Registry::class); - $this->_coreRegistry->register(RegistryConstants::CURRENT_CUSTOMER_ID, self::CUSTOMER_ID_VALUE); - - $this->_block = $this->_objectManager->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock( - \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart::class, - '', - ['context' => $this->_context, 'registry' => $this->_coreRegistry] - ); + $this->processCheckQuoteItems('customer_uk_address@test.com'); } /** - * @inheritdoc + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoDataFixture Magento/Customer/_files/two_customers.php + * @return void */ - protected function tearDown(): void + public function testCustomerWithoutQuoteView(): void { - $this->_coreRegistry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->processCheckWithoutQuoteItems('customer_two@example.com'); } /** @@ -95,23 +52,23 @@ protected function tearDown(): void */ public function testVerifyCollectionWithQuote(int $customerId, bool $guest, bool $contains): void { - $session = $this->_objectManager->create(SessionQuote::class); + $session = $this->objectManager->create(SessionQuote::class); $session->setCustomerId($customerId); - $quoteFixture = $this->_objectManager->create(Quote::class); + $quoteFixture = $this->objectManager->create(Quote::class); $quoteFixture->load('test01', 'reserved_order_id'); $quoteFixture->setCustomerIsGuest($guest) ->setCustomerId($customerId) ->save(); - $this->_block->toHtml(); + $this->block->toHtml(); if ($contains) { $this->assertStringContainsString( "We couldn't find any records", - $this->_block->getGridParentHtml() + $this->block->getGridParentHtml() ); } else { $this->assertStringNotContainsString( "We couldn't find any records", - $this->_block->getGridParentHtml() + $this->block->getGridParentHtml() ); } } @@ -144,7 +101,7 @@ public function getQuoteDataProvider(): array */ public function testGetCustomerId(): void { - $this->assertEquals(self::CUSTOMER_ID_VALUE, $this->_block->getCustomerId()); + $this->assertEquals(self::CUSTOMER_ID_VALUE, $this->block->getCustomerId()); } /** @@ -154,7 +111,7 @@ public function testGetCustomerId(): void */ public function testGetGridUrl(): void { - $this->assertStringContainsString('/backend/customer/index/cart', $this->_block->getGridUrl()); + $this->assertStringContainsString('/backend/customer/index/cart', $this->block->getGridUrl()); } /** @@ -164,20 +121,13 @@ public function testGetGridUrl(): void */ public function testGetGridParentHtml(): void { - $this->_block = $this->_objectManager->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock( - \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart::class, - '', - [] - ); $mockCollection = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) ->disableOriginalConstructor() ->getMock(); - $this->_block->setCollection($mockCollection); + $this->block->setCollection($mockCollection); $this->assertStringContainsString( "<div class=\"admin__data-grid-header admin__data-grid-toolbar\"", - $this->_block->getGridParentHtml() + $this->block->getGridParentHtml() ); } @@ -190,7 +140,7 @@ public function testGetRowUrl(): void { $row = new \Magento\Framework\DataObject(); $row->setProductId(1); - $this->assertStringContainsString('/backend/catalog/product/edit/id/1', $this->_block->getRowUrl($row)); + $this->assertStringContainsString('/backend/catalog/product/edit/id/1', $this->block->getRowUrl($row)); } /** @@ -200,7 +150,7 @@ public function testGetRowUrl(): void */ public function testGetHtml(): void { - $html = $this->_block->toHtml(); + $html = $this->block->toHtml(); $this->assertStringContainsString("<div id=\"customer_cart_grid\"", $html); $this->assertStringContainsString("<div class=\"admin__data-grid-header admin__data-grid-toolbar\"", $html); $this->assertStringContainsString("customer_cart_gridJsObject = new varienGrid(\"customer_cart_grid\",", $html); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/AbstractItemTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/AbstractItemTest.php new file mode 100644 index 0000000000000..82a1f3647b786 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/AbstractItemTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Helper\Product\Configuration; +use Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item as RendererItem; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Base class for testing \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item block + */ +abstract class AbstractItemTest extends TestCase +{ + /** @var ObjectManager */ + protected $objectManager; + + /** @var RendererItem */ + private $blockRendererItem; + + /** @var CollectionFactory */ + private $quoteItemCollectionFactory; + + /** @var Configuration */ + private $productConfiguration; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->blockRendererItem = $this->objectManager->get(LayoutInterface::class)->createBlock(RendererItem::class); + $this->quoteItemCollectionFactory = $this->objectManager->get(CollectionFactory::class); + $this->productConfiguration = $this->objectManager->get(Configuration::class); + } + + /** + * Check item block rendering + * + * @return void + */ + protected function processRender(): void + { + $itemsCollection = $this->quoteItemCollectionFactory->create(); + /** @var Item $quoteItem */ + $quoteItem = $itemsCollection->getFirstItem(); + $this->assertNotEmpty($quoteItem->getId()); + $this->blockRendererItem->setProductHelpers([]); + $html = $this->blockRendererItem->render($quoteItem); + + $this->assertRendererItemValue($quoteItem, $html); + } + + /** + * Check that the product name and options are in the block. + * + * @param Item $quoteItem + * @param string $html + * @return void + */ + private function assertRendererItemValue(Item $quoteItem, string $html): void + { + $optionsXPath = $this->getOptionsValueXPath($quoteItem); + $productName = $quoteItem->getProduct()->getName(); + + $productNameXPath = count($optionsXPath) === 0 ? "/descendant::*[contains(text(), '$productName')]" + : "//div[contains(@class, 'product-title') and contains(text(), '$productName')]"; + + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($productNameXPath, $html), + 'The block\'s rendered value does not contain expected product name.' + ); + foreach ($optionsXPath as $option) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($option['xpath'], $html), + sprintf('The block\'s rendered value does not contain expected option. Option: %s', $option['label']) + ); + } + } + + /** + * Get item options and their xpath expression + * + * @param Item $quoteItem + * @return array + */ + private function getOptionsValueXPath(Item $quoteItem): array + { + $options = $this->productConfiguration->getOptions($quoteItem); + foreach ($options as $key => $option) { + $options[$key]['xpath'] = "//dl[contains(@class, 'item-options')]" + . "/dt[contains(text(), '{$option['label']}')]" + . "/following-sibling::dd[1]"; + + if (isset($option['option_type']) + && $option['option_type'] == ProductCustomOptionInterface::OPTION_GROUP_FILE) { + $value = explode(" ", $option['print_value']); + $options[$key]['xpath'] .= "/a[contains(text(), '{$value[0]}')]"; + } else { + $options[$key]['xpath'] .= "[contains(text(), '{$option['value']}')]"; + } + } + + return $options; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/Item/ItemConfigurableTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/Item/ItemConfigurableTest.php new file mode 100644 index 0000000000000..adcae7c5b434d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/Item/ItemConfigurableTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item; + +use Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\AbstractItemTest; +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks item block rendering with configurable product. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item + */ +class ItemConfigurableTest extends AbstractItemTest +{ + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_ConfigurableProduct + if (!$moduleManager->isEnabled('Magento_ConfigurableProduct')) { + self::markTestSkipped('Magento_ConfigurableProduct module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php + * @return void + */ + public function testRenderConfigurableProduct(): void + { + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php new file mode 100644 index 0000000000000..1a26d22b4cc5b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Grid/Renderer/ItemTest.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer; + +/** + * Class checks item block rendering with simple product and simple product with options. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Grid\Renderer\Item + */ +class ItemTest extends AbstractItemTest +{ + /** + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * @return void + */ + public function testRenderProductOptions(): void + { + $this->processRender(); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @return void + */ + public function testRenderSimpleProduct(): void + { + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php new file mode 100644 index 0000000000000..11d51e1f2c814 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.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\Block\Adminhtml\Edit\Tab\View; + +use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for customer wish list tab. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class WishlistTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + /** @var Wishlist */ + private $block; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Wishlist::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * + * @return void + */ + public function testWishListGrid(): void + { + $this->registerCustomerId(1); + $this->assertCount(1, $this->block->getPreparedCollection()); + } + + /** + * Add customer id to registry. + * + * @param int $customerId + * @return void + */ + private function registerCustomerId(int $customerId): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->registry->register(RegistryConstants::CURRENT_CUSTOMER_ID, $customerId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php new file mode 100644 index 0000000000000..f8ede749872f4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Grid\Renderer; + +use Magento\Backend\Block\Widget\Grid\Column\Extended; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Base class for testing \Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction block + */ +abstract class AbstractMultiactionTest extends TestCase +{ + /** @var ObjectManager */ + protected $objectManager; + + /** @var Extended */ + protected $blockColumn; + + /** @var Multiaction */ + protected $blockMultiaction; + + /** @var LayoutInterface */ + private $layout; + + /** @var CollectionFactory */ + private $quoteItemCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->blockColumn = $this->layout->createBlock(Extended::class); + $this->blockColumn->setData([ + 'header' => 'Action', + 'index' => 'item_id', + 'renderer' => Multiaction::class, + 'filter' => false, + 'sortable' => false, + ]); + $this->blockMultiaction = $this->layout->createBlock(Multiaction::class); + $this->quoteItemCollectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * Check multiaction block rendering + * + * @return void + */ + protected function processRender(): void + { + $itemsCollection = $this->quoteItemCollectionFactory->create(); + /** @var Item $quoteItem */ + $quoteItem = $itemsCollection->getFirstItem(); + $this->assertNotEmpty($quoteItem->getId()); + $actions = [ + [ + 'caption' => 'configure', + 'url' => 'url_configureItem', + 'process' => 'configurable', + 'control_object' => 'cartControl', + ], + [ + 'caption' => 'delete', + 'url' => 'url_removeItem', + 'onclick' => 'return cartControl.removeItem($item_id);' + ], + ]; + $this->blockColumn->addData(['actions' => $actions]); + $this->blockMultiaction->setColumn($this->blockColumn); + $html = $this->blockMultiaction->render($quoteItem); + + foreach ($actions as $action) { + $this->assertUrl((int)$quoteItem->getId(), $action, $html); + } + } + + /** + * Check that the link in the block is correct + * + * @param int $quoteItemId + * @param array $action + * @param string $html + * @return void + */ + private function assertUrl(int $quoteItemId, array $action, string $html): void + { + $jsFunction = str_replace('url_', '', $action['url']); + $configureXPath = "//a[contains(@onclick, 'return cartControl.$jsFunction($quoteItemId)')" + . " and text()='{$action['caption']}' and @href='{$action['url']}']"; + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($configureXPath, $html), + sprintf('Expected %s link is incorrect or missing', $action['caption']) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionBundleTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionBundleTest.php new file mode 100644 index 0000000000000..2e2385c5088f2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionBundleTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction; + +use Magento\Customer\Block\Adminhtml\Grid\Renderer\AbstractMultiactionTest; +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks multiaction block rendering with bundle product. + * + * @see \Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction + */ +class MultiactionBundleTest extends AbstractMultiactionTest +{ + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_Bundle + if (!$moduleManager->isEnabled('Magento_Bundle')) { + self::markTestSkipped('Magento_Bundle module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/Bundle/_files/quote_with_bundle_and_options.php + * @return void + */ + public function testRenderConfigurableProduct(): void + { + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionConfigurableTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionConfigurableTest.php new file mode 100644 index 0000000000000..ae279552c122a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction/MultiactionConfigurableTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction; + +use Magento\Customer\Block\Adminhtml\Grid\Renderer\AbstractMultiactionTest; +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class checks multiaction block rendering with configurable product. + * + * @see \Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction + */ +class MultiactionConfigurableTest extends AbstractMultiactionTest +{ + /** + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Manager $moduleManager */ + $moduleManager = $objectManager->get(Manager::class); + //This check is needed because Customer independent of Magento_ConfigurableProduct + if (!$moduleManager->isEnabled('Magento_ConfigurableProduct')) { + self::markTestSkipped('Magento_ConfigurableProduct module disabled.'); + } + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/customer_quote_with_items_configurable_product.php + * @return void + */ + public function testRenderConfigurableProduct(): void + { + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/MultiactionTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/MultiactionTest.php new file mode 100644 index 0000000000000..430fba8458c29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/MultiactionTest.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\Block\Adminhtml\Grid\Renderer; + +use Magento\Framework\DataObject; + +/** + * Class checks multiaction block rendering with simple product and simple product with options. + * + * @see \Magento\Customer\Block\Adminhtml\Grid\Renderer\Multiaction + */ +class MultiactionTest extends AbstractMultiactionTest +{ + /** + * @dataProvider renderEmptyProvider + * @param array $columnData + * @return void + */ + public function testRenderEmpty(array $columnData): void + { + /** @var DataObject $row */ + $row = $this->objectManager->create(DataObject::class); + $this->blockColumn->addData($columnData); + $this->blockMultiaction->setColumn($this->blockColumn); + $this->assertEquals( + ' ', + $this->blockMultiaction->render($row) + ); + } + + /** + * Data provider for testRenderEmpty + * + * @return array + */ + public function renderEmptyProvider(): array + { + return [ + 'empty_actions' => [ + 'column_data' => ['actions' => []], + ], + 'not_array_actions' => [ + 'column_data' => ['actions' => 'actions'], + ], + 'empty_actions_element' => [ + 'column_data' => [ + 'actions' => [ + 'action_1' => 'actions', + ], + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * @return void + */ + public function testRenderProductOptions(): void + { + $this->processRender(); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @return void + */ + public function testRenderSimpleProduct(): void + { + $this->markTestSkipped('Test is blocked by issue MC-34612'); + $this->processRender(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php index 6788d44d8e536..5c4a29f0a9d32 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php @@ -89,4 +89,24 @@ public function testLoginForm(): void 'Forgot password link does not exist on the page' ); } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 1 + * + * @return void + */ + public function testAutocompletePasswordEnabled(): void + { + $this->assertFalse($this->block->isAutocompleteDisabled()); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 0 + * + * @return void + */ + public function testAutocompletePasswordDisabled(): void + { + $this->assertTrue($this->block->isAutocompleteDisabled()); + } } 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/Controller/Adminhtml/Cart/Product/Composite/Cart/ConfigureTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/ConfigureTest.php new file mode 100644 index 0000000000000..87a4eb15f1913 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/ConfigureTest.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Adminhtml\Cart\Product\Composite\Cart; + +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Tests for configure quote item in customer shopping cart. + * + * @magentoAppArea adminhtml + */ +class ConfigureTest extends AbstractBackendController +{ + /** @var CollectionFactory */ + private $quoteItemCollectionFactory; + + /** @var int */ + private $baseWebsiteId; + + /** @var SerializerInterface */ + private $json; + + /** @inheritdoc */ + public function setUp(): void + { + parent::setUp(); + $this->quoteItemCollectionFactory = $this->_objectManager->get(CollectionFactory::class); + $this->baseWebsiteId = (int)$this->_objectManager->get(StoreManagerInterface::class) + ->getWebsite('base') + ->getId(); + $this->json = $this->_objectManager->get(SerializerInterface::class); + } + + /** + * @return void + */ + public function testConfigureActionNoCustomerId(): void + { + $this->dispatchCompositeCartConfigure(); + $this->assertEquals( + [ + 'error' => true, + 'message' => "The customer ID isn't defined.", + ], + $this->json->unserialize($this->getResponse()->getBody()) + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void + */ + public function testConfigureNoQuoteId(): void + { + $this->dispatchCompositeCartConfigure([ + 'customer_id' => 1, + 'website_id' => $this->baseWebsiteId, + ]); + $this->assertEquals( + [ + 'error' => true, + 'message' => "The quote items are incorrect. Verify the quote items and try again.", + ], + $this->json->unserialize($this->getResponse()->getBody()) + ); + } + + /** + * @dataProvider configureWithQuoteProvider + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/quote.php + * @param bool $hasQuoteItem + * @param string $expectedResponseBody + * @return void + */ + public function testConfigureWithQuote(bool $hasQuoteItem, string $expectedResponseBody): void + { + $itemsCollection = $this->quoteItemCollectionFactory->create(); + $itemId = $itemsCollection->getFirstItem()->getId(); + $this->assertNotEmpty($itemId); + if (!$hasQuoteItem) { + $itemId++; + } + $this->dispatchCompositeCartConfigure([ + 'customer_id' => 1, + 'website_id' => $this->baseWebsiteId, + 'id' => $itemId, + ]); + $this->assertStringContainsString( + $expectedResponseBody, + $this->getResponse()->getBody() + ); + } + + /** + * Create configure with quote provider + * + * @return array + */ + public function configureWithQuoteProvider(): array + { + return [ + 'with_quote_item_id' => [ + 'has_quote_item' => true, + 'expected_response_body' => '<input id="product_composite_configure_input_qty"' + . ' class="input-text admin__control-text qty" type="text" name="qty" value="1">', + ], + 'without_quote_item_id' => [ + 'has_quote_item' => false, + 'expected_response_body' => '{"error":true,"message":"The quote items are incorrect.' + . ' Verify the quote items and try again."}', + ], + ]; + } + + /** + * Dispatch configure quote item in customer shopping cart + * using backend/customer/cart_product_composite_cart/configure action. + * + * @param array $params + * @param array $postValue + * @return void + */ + private function dispatchCompositeCartConfigure(array $params = [], array $postValue = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($postValue); + $this->dispatch('backend/customer/cart_product_composite_cart/configure'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/UpdateTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/UpdateTest.php new file mode 100644 index 0000000000000..fd0e7a8d95833 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/Cart/UpdateTest.php @@ -0,0 +1,325 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Adminhtml\Cart\Product\Composite\Cart; + +use Magento\Backend\Model\Session; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Model\Product\Option as ProductOption; +use Magento\Catalog\Model\Product\Option\Type\File\ValidatorInfo; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\DataObject; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Tests for update quote item in customer shopping cart. + * + * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class UpdateTest extends AbstractBackendController +{ + /** @var CollectionFactory */ + private $quoteItemCollectionFactory; + + /** @var Session */ + private $session; + + /** @var SerializerInterface */ + private $json; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var int */ + private $baseWebsiteId; + + /** @inheritdoc */ + public function setUp(): void + { + parent::setUp(); + $this->quoteItemCollectionFactory = $this->_objectManager->get(CollectionFactory::class); + $this->session = $this->_objectManager->get(Session::class); + $this->json = $this->_objectManager->get(SerializerInterface::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->baseWebsiteId = (int)$this->_objectManager->get(StoreManagerInterface::class) + ->getWebsite('base') + ->getId(); + } + + /** + * @return void + */ + public function testUpdateNoCustomerId(): void + { + $expectedUpdateResult = [ + 'error' => true, + 'message' => (string)__("The customer ID isn't defined."), + 'js_var_name' => null, + ]; + $this->dispatchCompositeCartUpdate(); + /** @var DataObject $updateResult */ + $updateResult = $this->session->getCompositeProductResult(); + $this->assertEquals($expectedUpdateResult, $updateResult->getData()); + $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void + */ + public function testUpdateNoQuoteId(): void + { + $expectedUpdateResult = [ + 'error' => true, + 'message' => (string)__('The quote items are incorrect. Verify the quote items and try again.'), + 'js_var_name' => 'iFrameResponse', + ]; + $this->dispatchCompositeCartUpdate([ + 'customer_id' => 1, + 'website_id' => $this->baseWebsiteId, + 'as_js_varname' => 'iFrameResponse', + ]); + /** @var DataObject $updateResult */ + $updateResult = $this->session->getCompositeProductResult(); + $this->assertEquals($expectedUpdateResult, $updateResult->getData()); + $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); + } + + /** + * @dataProvider updateWithQuoteProvider + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/quote.php + * @param bool $hasQuoteItem + * @param array $expectedUpdateResult + * @return void + */ + public function testUpdateWithQuote(bool $hasQuoteItem, array $expectedUpdateResult): void + { + $itemsCollection = $this->quoteItemCollectionFactory->create(); + $itemId = $itemsCollection->getFirstItem()->getId(); + $this->assertNotEmpty($itemId); + if (!$hasQuoteItem) { + $itemId++; + } + $this->dispatchCompositeCartUpdate( + [ + 'customer_id' => 1, + 'website_id' => $this->baseWebsiteId, + ], + [ + 'id' => $itemId, + 'as_js_varname' => 'iFrameResponse', + 'qty' => 20, + ] + ); + /** @var DataObject $updateResult */ + $updateResult = $this->session->getCompositeProductResult(); + $this->assertEquals($expectedUpdateResult, $updateResult->getData()); + $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); + } + + /** + * Create update with quote provider + * + * @return array + */ + public function updateWithQuoteProvider(): array + { + return [ + 'with_quote_item_id' => [ + 'has_quote_item' => true, + 'expected_update_result' => [ + 'ok' => true, + 'js_var_name' => 'iFrameResponse', + ], + ], + 'without_quote_item_id' => [ + 'has_quote_item' => false, + 'expected_update_result' => [ + 'error' => true, + 'message' => (string)__('The quote items are incorrect. Verify the quote items and try again.'), + 'js_var_name' => 'iFrameResponse', + ], + ], + ]; + } + + /** + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * @return void + */ + public function testUpdateSimpleProductOption(): void + { + $customer = $this->customerRepository->get('customer_uk_address@test.com'); + /** @var Quote $quote */ + $quote = $this->quoteRepository->getForCustomer($customer->getId()); + /** @var QuoteItem $quoteItem */ + $quoteItem = $quote->getItemsCollection()->getFirstItem(); + $this->assertNotEmpty($quoteItem->getId()); + $expectedData = $this->prepareExpectedData($quoteItem); + $expectedUpdateResult = [ + 'ok' => true, + 'js_var_name' => 'iFrameResponse', + ]; + $expectedParams = [ + 'id' => $quoteItem->getId(), + 'as_js_varname' => 'iFrameResponse', + 'options' => $expectedData['options'], + 'qty' => 5, + ]; + $this->dispatchCompositeCartUpdate( + [ + 'customer_id' => $customer->getId(), + 'website_id' => $customer->getWebsiteId(), + ], + $expectedParams + ); + /** @var DataObject $updateResult */ + $updateResult = $this->session->getCompositeProductResult(); + $this->assertEquals($expectedUpdateResult, $updateResult->getData()); + + $quoteItem = $this->getQuoteItemBySku($quote, $expectedData['sku']); + $this->assertNotNull($quoteItem, 'Missing expected shopping cart item after update.'); + $this->assertQuoteItemOptions($quoteItem, $expectedParams); + $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); + } + + /** + * Prepare quote item options and sku for update. + * + * @param QuoteItem $quoteItem + * @return array + */ + private function prepareExpectedData(QuoteItem $quoteItem): array + { + $buyRequest = $this->json->unserialize($quoteItem->getOptionByCode('info_buyRequest')->getValue()); + $productOptions = $quoteItem->getProduct()->getOptions(); + $options = []; + $sku = $quoteItem->getSku(); + /** @var ProductOption $productOption */ + foreach ($productOptions as $productOption) { + $itemOptionValue = $buyRequest['options'][$productOption->getId()]; + switch ($productOption->getType()) { + case ProductCustomOptionInterface::OPTION_TYPE_RADIO: + $productValues = $productOption->getValues(); + $currentRadioSku = $productValues[$itemOptionValue]->getSku(); + unset($productValues[$itemOptionValue]); + $value = (string)key($productValues); + $newRadioSku = $productValues[$value]->getSku(); + $sku = str_replace($currentRadioSku, $newRadioSku, $sku); + break; + case ProductCustomOptionInterface::OPTION_TYPE_DATE: + $value = ['year' => 2019, 'month' => 8, 'day' => 9, 'hour' => 13, 'minute' => 35]; + break; + case ProductCustomOptionInterface::OPTION_TYPE_FILE: + $itemOptionValue['title'] = 'testcart.jpg'; + $value = $itemOptionValue; + $validatorInfoMock = $this->prepareValidatorInfoMock(); + $this->_objectManager->addSharedInstance($validatorInfoMock, ValidatorInfo::class); + break; + case ProductCustomOptionInterface::OPTION_TYPE_AREA: + $value = 'testcart'; + break; + default: + $value = $itemOptionValue; + break; + } + $options[$productOption->getId()] = $value; + } + + return [ + 'options' => $options, + 'sku' => $sku, + ]; + } + + /** + * Prepare mock for updating file type options. + * + * @return MockObject + */ + private function prepareValidatorInfoMock(): MockObject + { + $validatorInfoMock = $this->createMock(ValidatorInfo::class); + $validatorInfoMock->method('setUseQuotePath')->willReturnSelf(); + $validatorInfoMock->expects($this->any()) + ->method('validate') + ->willReturn(true); + + return $validatorInfoMock; + } + + /** + * Get quote item by sku. + * + * @param Quote $quote + * @param string $sku + * @return QuoteItem|null + */ + private function getQuoteItemBySku(Quote $quote, string $sku): ?QuoteItem + { + $itemsCollection = $quote->getItemsCollection(false); + $itemsCollection->addFieldToFilter('sku', $sku); + /** @var QuoteItem $quoteItem */ + $quoteItem = $itemsCollection->getFirstItem(); + + return empty($quoteItem->getId()) ? null : $quoteItem; + } + + /** + * Verify that the quote item options are saved successfully. + * + * @param QuoteItem $quoteItem + * @param array $expectedParams + * @return void + */ + private function assertQuoteItemOptions(QuoteItem $quoteItem, array $expectedParams): void + { + $buyRequest = $this->json->unserialize($quoteItem->getOptionByCode('info_buyRequest')->getValue()); + foreach ($expectedParams as $key => $value) { + if ($key == 'options') { + foreach ($value as $optionId => $optionValue) { + $buyRequestValue = is_array($optionValue) + ? array_intersect_assoc($optionValue, $buyRequest[$key][$optionId]) + : $buyRequest[$key][$optionId]; + $this->assertEquals($optionValue, $buyRequestValue); + } + } else { + $this->assertEquals($value, $buyRequest[$key]); + } + } + } + + /** + * Dispatch update quote item in customer shopping cart + * using backend/customer/cart_product_composite_cart/update action. + * + * @param array $params + * @param array $postValue + * @return void + */ + private function dispatchCompositeCartUpdate(array $params = [], array $postValue = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($postValue); + $this->dispatch('backend/customer/cart_product_composite_cart/update'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/CartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/CartTest.php deleted file mode 100644 index ea21b2df663d9..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Cart/Product/Composite/CartTest.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Customer\Controller\Adminhtml\Cart\Product\Composite; - -/** - * @magentoAppArea adminhtml - */ -class CartTest extends \Magento\TestFramework\TestCase\AbstractBackendController -{ - /** - * @var \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory - */ - protected $quoteItemCollectionFactory; - - protected function setUp(): void - { - parent::setUp(); - $this->quoteItemCollectionFactory = $this->_objectManager->get( - \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory::class - ); - } - - public function testConfigureActionNoCustomerId() - { - $this->dispatch('backend/customer/cart_product_composite_cart/configure'); - $this->assertEquals( - '{"error":true,"message":"The customer ID isn\'t defined."}', - $this->getResponse()->getBody() - ); - } - - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - */ - public function testConfigureActionNoQuoteId() - { - $this->getRequest()->setParam('customer_id', 1); - $this->getRequest()->setParam('website_id', 1); - $this->dispatch('backend/customer/cart_product_composite_cart/configure'); - $this->assertEquals( - '{"error":true,"message":"The quote items are incorrect. Verify the quote items and try again."}', - $this->getResponse()->getBody() - ); - } - - /** - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoDataFixture Magento/Customer/_files/quote.php - */ - public function testConfigureAction() - { - $items = $this->quoteItemCollectionFactory->create(); - $itemId = $items->getAllIds()[0]; - $this->getRequest()->setParam('customer_id', 1); - $this->getRequest()->setParam('website_id', 1); - $this->getRequest()->setParam('id', $itemId); - $this->dispatch('backend/customer/cart_product_composite_cart/configure'); - $this->assertStringContainsString( - '<input id="product_composite_configure_input_qty" class="input-text admin__control-text qty"' - . ' type="text" name="qty" value="1">', - $this->getResponse()->getBody() - ); - } - - public function testUpdateActionNoCustomerId() - { - $this->dispatch('backend/customer/cart_product_composite_cart/update'); - $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); - } - - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - */ - public function testUpdateActionNoQuoteId() - { - $this->getRequest()->setParam('customer_id', 1); - $this->getRequest()->setParam('website_id', 1); - $this->dispatch('backend/customer/cart_product_composite_cart/update'); - $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); - } - - /** - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoDataFixture Magento/Customer/_files/quote.php - */ - public function testUpdateAction() - { - $items = $this->quoteItemCollectionFactory->create(); - $itemId = $items->getAllIds()[0]; - $this->getRequest()->setParam('customer_id', 1); - $this->getRequest()->setParam('website_id', 1); - $this->getRequest()->setParam('id', $itemId); - - $this->dispatch('backend/customer/cart_product_composite_cart/update'); - $this->assertRedirect($this->stringContains('catalog/product/showUpdateResult')); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/CartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/CartTest.php new file mode 100644 index 0000000000000..3e2652f0a0709 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/CartTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Adminhtml\Index; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Class checks customer's shopping cart controller. + * + * @see \Magento\Customer\Controller\Adminhtml\Index\Cart + * @magentoAppArea adminhtml + */ +class CartTest extends AbstractBackendController +{ + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_sample.php + * @return void + */ + public function testCartAction(): void + { + $this->dispatchShoppingCart( + [ + 'id' => 1, + 'website_id' => 1, + ], + ['delete' => 1] + ); + $body = $this->getResponse()->getBody(); + $this->assertStringContainsString('<div id="customer_cart_grid"', $body); + } + + /** + * Delete customer shopping cart item + * + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + * @return void + */ + public function testDeleteCartItem(): void + { + $customer = $this->customerRepository->get('customer_uk_address@test.com'); + /** @var Quote $quote */ + $quote = $this->quoteRepository->getForCustomer($customer->getId()); + $quoteItemId = $quote->getItemsCollection()->getFirstItem()->getItemId(); + $this->assertNotEmpty($quoteItemId); + $this->dispatchShoppingCart( + [ + 'id' => $customer->getId(), + 'website_id' => $customer->getWebsiteId(), + ], + ['delete' => $quoteItemId] + ); + $quote->getItemsCollection(false); + $this->assertFalse( + $quote->getItemById($quoteItemId), + sprintf('Customer\'s shopping cart item with ID = %s has not been deleted', $quoteItemId) + ); + } + + /** + * Dispatch admin shopping cart using backend/customer/index/cart action. + * + * @param array $params + * @param array $postValue + * @return void + */ + private function dispatchShoppingCart(array $params = [], array $postValue = []): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($postValue); + $this->dispatch('backend/customer/index/cart'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php index 019c1c277e55f..40c84d8b5db58 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php @@ -153,17 +153,6 @@ public function te1stNewActionWithCustomerData() $this->testNewAction(); } - /** - * @magentoDataFixture Magento/Customer/_files/customer_sample.php - */ - public function testCartAction() - { - $this->getRequest()->setParam('id', 1)->setParam('website_id', 1)->setPostValue('delete', 1); - $this->dispatch('backend/customer/index/cart'); - $body = $this->getResponse()->getBody(); - $this->assertStringContainsString('<div id="customer_cart_grid"', $body); - } - /** * @magentoDbIsolation enabled */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Wishlist/Product/Composite/Wishlist/UpdateTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Wishlist/Product/Composite/Wishlist/UpdateTest.php new file mode 100644 index 0000000000000..035db789b171e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Wishlist/Product/Composite/Wishlist/UpdateTest.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Adminhtml\Wishlist\Product\Composite\Wishlist; + +use Magento\Backend\Model\Session; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\TestFramework\Wishlist\Model\GetWishlistByCustomerId; +use Magento\Wishlist\Model\Item; + +/** + * Tests for update wish list items. + * + * @magentoAppArea adminhtml + */ +class UpdateTest extends AbstractBackendController +{ + /** @var GetWishlistByCustomerId */ + private $getWishlistByCustomerId; + + /** @var SerializerInterface */ + private $json; + + /** @var Session */ + private $session; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->getWishlistByCustomerId = $this->_objectManager->get(GetWishlistByCustomerId::class); + $this->json = $this->_objectManager->get(SerializerInterface::class); + $this->session = $this->_objectManager->get(Session::class); + } + + /** + * @magentoDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testUpdateItem(): void + { + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'simple-1'); + $this->assertNotNull($item); + $params = ['id' => $item->getId(), 'qty' => 5]; + $this->dispatchUpdateItemRequest($params); + $this->assertEquals($params['qty'], $this->getWishlistByCustomerId->getItemBySku(1, 'simple-1')->getQty()); + } + + /** + * @magentoDataFixture Magento/Wishlist/_files/wishlist_with_configurable_product.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testUpdateItemOption(): void + { + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'Configurable product'); + $this->assertNotNull($item); + $params = [ + 'id' => $item->getId(), + 'super_attribute' => $this->performConfigurableOption($item->getProduct()), + 'qty' => 5, + ]; + $this->dispatchUpdateItemRequest($params); + $this->assertUpdatedItem( + $this->getWishlistByCustomerId->getItemBySku(1, 'Configurable product'), + $params + ); + } + + /** + * @return void + */ + public function testUpdateNotExistingItem(): void + { + $this->dispatchUpdateItemRequest(['id' => 989]); + $this->assertTrue($this->session->getCompositeProductResult()->getError()); + $this->assertEquals( + (string)__('Please load Wish List item.'), + $this->session->getCompositeProductResult()->getMessage() + ); + } + + /** + * @return void + */ + public function testUpdateWithoutParams(): void + { + $this->dispatchUpdateItemRequest([]); + $this->assertTrue($this->session->getCompositeProductResult()->getError()); + $this->assertEquals( + (string)__('Please define Wish List item ID.'), + $this->session->getCompositeProductResult()->getMessage() + ); + } + /** + * Assert updated item in wish list. + * + * @param Item $item + * @param array $expectedData + * @return void + */ + private function assertUpdatedItem(Item $item, array $expectedData): void + { + $this->assertEquals($expectedData['qty'], $item->getQty()); + $buyRequestOption = $this->json->unserialize($item->getOptionByCode('info_buyRequest')->getValue()); + foreach ($expectedData as $key => $value) { + $this->assertEquals($value, $buyRequestOption[$key]); + } + } + + /** + * Perform configurable option to select. + * + * @param ProductInterface $product + * @return array + */ + private function performConfigurableOption(ProductInterface $product): array + { + $configurableOptions = $product->getTypeInstance()->getConfigurableOptions($product); + $attributeId = key($configurableOptions); + $option = reset($configurableOptions[$attributeId]); + + return [$attributeId => $option['value_index']]; + } + + /** + * Dispatch update wish list item request. + * + * @param array $params + * @return void + */ + private function dispatchUpdateItemRequest(array $params): void + { + $this->getRequest()->setParams($params)->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/customer/wishlist_product_composite_wishlist/update'); + $this->assertRedirect($this->stringContains('backend/catalog/product/showUpdateResult/')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/AuthenticateTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/AuthenticateTest.php new file mode 100644 index 0000000000000..345a001973e8c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/AuthenticateTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\AccountManagement; + +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\State\UserLockedException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for customer authenticate via customer account management service. + * + * @magentoDbIsolation enabled + */ +class AuthenticateTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var CustomerRegistry */ + private $customerRegistry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->accountManagement = $this->objectManager->get(AccountManagementInterface::class); + $this->customerRegistry = $this->objectManager->get(CustomerRegistry::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/locked_customer.php + * + * @return void + */ + public function testAuthenticateByLockedCustomer(): void + { + $this->expectExceptionObject(new UserLockedException(__('The account is locked.'))); + $this->accountManagement->authenticate('customer@example.com', 'password'); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/expired_lock_for_customer.php + * + * @return void + */ + public function testAuthenticateByCustomerExpiredLock(): void + { + $email = 'customer@example.com'; + $customer = $this->accountManagement->authenticate($email, 'password'); + $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId()); + $this->assertEquals(0, $customerSecure->getFailuresNum()); + $this->assertNull($customerSecure->getFirstFailure()); + $this->assertNull($customerSecure->getLockExpires()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AuthenticationTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AuthenticationTest.php new file mode 100644 index 0000000000000..3de64701bdedb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AuthenticationTest.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Framework\Exception\InvalidEmailOrPasswordException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for customer authentication model. + * + * @see \Magento\Customer\Model\Authentication + * @magentoDbIsolation enabled + */ +class AuthenticationTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var AuthenticationInterface */ + private $authentication; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->authentication = $this->objectManager->get(AuthenticationInterface::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/expired_lock_for_customer.php + * + * @return void + */ + public function testCustomerAuthenticate(): void + { + $this->assertTrue($this->authentication->authenticate(1, 'password')); + } + + /** + * @magentoDataFixture Magento/Customer/_files/expired_lock_for_customer.php + * + * @return void + */ + public function testCustomerAuthenticateWithWrongPassword(): void + { + $this->expectExceptionObject(new InvalidEmailOrPasswordException(__('Invalid login or password.'))); + $this->authentication->authenticate(1, 'password1'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Checkout/ConfigProviderTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Checkout/ConfigProviderTest.php new file mode 100644 index 0000000000000..f29b5f622cdf1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Checkout/ConfigProviderTest.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Checkout; + +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for customer checkout config provider. + * + * @see \Magento\Customer\Model\Checkout\ConfigProvider + */ +class ConfigProviderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var ConfigProvider */ + private $configProvider; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->configProvider = $this->objectManager->get(ConfigProvider::class); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 1 + * + * @return void + */ + public function testAutocompletePasswordEnabled(): void + { + $this->assertEquals('on', $this->configProvider->getConfig()['autocomplete']); + } + + /** + * @magentoConfigFixture current_store customer/password/autocomplete_on_storefront 0 + * + * @return void + */ + public function testAutocompletePasswordDisabled(): void + { + $this->assertEquals('off', $this->configProvider->getConfig()['autocomplete']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php new file mode 100644 index 0000000000000..e63c3d2761c49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php @@ -0,0 +1,193 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\Template; +use Magento\Email\Model\TemplateFactory; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\Mail\MessageInterface; +use Magento\Framework\Module\Manager; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use PHPUnit\Framework\TestCase; + +/** + * Test for customer email notification model. + * + * @see \Magento\Customer\Model\EmailNotification + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class EmailNotificationTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Manager */ + private $moduleManager; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var EmailNotificationInterface */ + private $emailNotification; + + /** @var TransportBuilderMock */ + private $transportBuilder; + + /** @var TemplateResource */ + private $templateResource; + + /** @var TemplateFactory */ + private $templateFactory; + + /** @var MutableScopeConfigInterface */ + private $mutableScopeConfig; + + /** @var CollectionFactory */ + private $templateCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->moduleManager = $this->objectManager->get(Manager::class); + //This check is needed because Magento_Customer independent of Magento_Email + if (!$this->moduleManager->isEnabled('Magento_Email')) { + $this->markTestSkipped('Magento_Email module disabled.'); + } + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->emailNotification = $this->objectManager->get(EmailNotificationInterface::class); + $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); + $this->templateResource = $this->objectManager->get(TemplateResource::class); + $this->templateFactory = $this->objectManager->get(TemplateFactory::class); + $this->mutableScopeConfig = $this->objectManager->get(MutableScopeConfigInterface::class); + $this->templateCollectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->moduleManager->isEnabled('Magento_Email')) { + $this->mutableScopeConfig->clean(); + $collection = $this->templateCollectionFactory->create(); + $template = $collection->addFieldToFilter('template_code', 'customer_password_email_template') + ->getFirstItem(); + if ($template->getId()) { + $this->templateResource->delete($template); + } + } + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testResetPasswordCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_RESET_PASSWORD_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $this->emailNotification->credentialsChanged($customer, $customer->getEmail(), true); + $expectedSender = ['name' => 'CustomerSupport', 'email' => 'support@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/password/forgot_email_identity custom1 + * + * @return void + */ + public function testForgotPasswordCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_FORGOT_EMAIL_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $this->emailNotification->passwordResetConfirmation($customer); + $expectedSender = ['name' => 'Custom 1', 'email' => 'custom1@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/password/forgot_email_identity custom2 + * + * @return void + */ + public function testRemindPasswordCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_REMIND_EMAIL_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $this->emailNotification->passwordReminder($customer); + $expectedSender = ['name' => 'Custom 2', 'email' => 'custom2@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * Assert message. + * + * @param array $expectedSender + * @return void + */ + private function assertMessage(array $expectedSender): void + { + $message = $this->transportBuilder->getSentMessage(); + $this->assertNotNull($message); + $this->assertMessageSender($message, $expectedSender); + $this->assertStringContainsString( + 'Text specially for check in test.', + $message->getBody()->getParts()[0]->getRawContent(), + 'Expected text wasn\'t found in message.' + ); + } + + /** + * Assert message sender. + * + * @param MessageInterface $message + * @param array $expectedSender + * @return void + */ + private function assertMessageSender(MessageInterface $message, array $expectedSender): void + { + $messageFrom = $message->getFrom(); + $this->assertNotNull($messageFrom); + $messageFrom = current($messageFrom); + $this->assertEquals($expectedSender['name'], $messageFrom->getName()); + $this->assertEquals($expectedSender['email'], $messageFrom->getEmail()); + } + + /** + * Set email template config. + * + * @param string $configPath + * @return void + */ + private function setEmailTemplateConfig(string $configPath): void + { + $template = $this->templateFactory->create(); + $template->setTemplateCode('customer_password_email_template') + ->setTemplateText(file_get_contents(__DIR__ . '/../_files/customer_password_email_template.html')) + ->setTemplateType(Template::TYPE_HTML); + $this->templateResource->save($template); + $this->mutableScopeConfig->setValue($configPath, $template->getId(), ScopeInterface::SCOPE_STORE, 'default'); + } +} 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/customer_password_email_template.html b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_password_email_template.html new file mode 100644 index 0000000000000..482fa79247a23 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_password_email_template.html @@ -0,0 +1,10 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +{{template config_path="design/email/header_template"}} +<p>Text specially for check in test.</p> +{{template config_path="design/email/footer_template"}} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer.php b/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer.php new file mode 100644 index 0000000000000..e7c8e5074664e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Stdlib\DateTime; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +/** @var CustomerAuthUpdate $customerAuthUpdate */ +$customerAuthUpdate = $objectManager->get(CustomerAuthUpdate::class); +$customerId = 1; + +$customerSecure = $customerRegistry->retrieveSecureData($customerId); +$dateTime = new \DateTimeImmutable(); +$customerSecure->setFailuresNum(10) + ->setFirstFailure($dateTime->modify('-15 minutes')->format(DateTime::DATETIME_PHP_FORMAT)) + ->setLockExpires($dateTime->modify('-5 minutes')->format(DateTime::DATETIME_PHP_FORMAT)); +$customerAuthUpdate->saveAuth($customerId); +$customerRegistry->remove($customerId); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_customer_rollback.php new file mode 100644 index 0000000000000..abd24344319a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/expired_lock_for_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/Customer/_files/customer_rollback.php'); 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/locked_customer.php b/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer.php new file mode 100644 index 0000000000000..b88f025db4d76 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Stdlib\DateTime; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +/** @var CustomerAuthUpdate $customerAuthUpdate */ +$customerAuthUpdate = $objectManager->get(CustomerAuthUpdate::class); +$customerId = 1; + +$customerSecure = $customerRegistry->retrieveSecureData($customerId); +$dateTime = new \DateTimeImmutable(); +$customerSecure->setFailuresNum(10) + ->setFirstFailure($dateTime->modify('-5 minutes')->format(DateTime::DATETIME_PHP_FORMAT)) + ->setLockExpires($dateTime->modify('+5 minutes')->format(DateTime::DATETIME_PHP_FORMAT)); +$customerAuthUpdate->saveAuth($customerId); +$customerRegistry->remove($customerId); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/locked_customer_rollback.php new file mode 100644 index 0000000000000..abd24344319a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/locked_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/Customer/_files/customer_rollback.php'); 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/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php new file mode 100644 index 0000000000000..a5c88fc7571a2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\BatchDataMapper; + +use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test product data mapper + */ +class ProductDataMapperTest extends TestCase +{ + /** + * @var ProductDataMapper + */ + private $model; + /** + * @var Config + */ + private $eavConfig; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $additionalFieldsProvider = $this->createMock(AdditionalFieldsProviderInterface::class); + $additionalFieldsProvider->method('getFields')->willReturn([]); + $this->model = $this->objectManager->create( + ProductDataMapper::class, + [ + 'additionalFieldsProvider' => $additionalFieldsProvider + ] + ); + $this->eavConfig = $this->objectManager->get(Config::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + } + + /** + * Test mapping select attribute with different store labels + * + * @return void + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Elasticsearch/_files/select_attribute_store_labels.php + * @magentoConfigFixture default/catalog/search/engine elasticsearch + */ + public function testMapSelectAttributeWithDifferentStoreLabels(): void + { + $product = $this->productRepository->get('simple'); + $productId = $product->getId(); + $attribute = $this->eavConfig->getAttribute(Product::ENTITY, 'select_attribute'); + $defaultStore = $this->storeManager->getStore('default'); + $secondStore = $this->storeManager->getStore('fixture_second_store'); + $attributeId = $attribute->getId(); + $attributeValue = $this->getAttributeOptionValue($attribute, 'Table'); + $defaultStoreMap = [ + $productId => [ + 'store_id' => $defaultStore->getId(), + 'select_attribute' => $attributeValue, + 'select_attribute_value' => 'Table_default', + ] + ]; + $secondStoreMap = [ + $productId => [ + 'store_id' => $secondStore->getId(), + 'select_attribute' => $attributeValue, + 'select_attribute_value' => 'Table_fixture_second_store', + ] + ]; + $data = [ + $productId => [ + $attributeId => $attributeValue + ] + ]; + $this->assertEquals($defaultStoreMap, $this->model->map($data, $defaultStore->getId(), [])); + $this->assertEquals($secondStoreMap, $this->model->map($data, $secondStore->getId(), [])); + } + + /** + * Get attribute option value + * + * @param AbstractAttribute $attribute + * @param string $text + * @return string|null + */ + private function getAttributeOptionValue( + AbstractAttribute $attribute, + string $text + ): ?string { + $value = null; + $attribute->setStoreId(0); + foreach ($attribute->getOptions() as $option) { + if ($option->getLabel() === $text) { + $value = $option->getValue(); + break; + } + } + return $value; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php index 1eb2550dc484c..0173a643dd7bd 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php @@ -19,6 +19,7 @@ use Magento\Indexer\Model\Indexer; use Magento\Framework\Search\EngineResolverInterface; use Magento\TestModuleCatalogSearch\Model\ElasticsearchVersionChecker; +use PHPUnit\Framework\TestCase; /** * Important: Please make sure that each integration test file works with unique elastic search index. In order to @@ -29,7 +30,7 @@ * @magentoDataFixture Magento/Elasticsearch/_files/indexer.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class IndexHandlerTest extends \PHPUnit\Framework\TestCase +class IndexHandlerTest extends TestCase { /** * @var string @@ -116,6 +117,9 @@ public function testReindexAll(): void $products = $this->searchByName('Simple Product', $storeId); $this->assertCount(5, $products); + + $this->assertCount(2, $this->searchByBoolAttribute(0, $storeId)); + $this->assertCount(3, $this->searchByBoolAttribute(1, $storeId)); } } @@ -266,6 +270,32 @@ private function searchByName(string $text, int $storeId): array return $products; } + /** + * Search docs in Elasticsearch by boolean attribute. + * + * @param int $value + * @param int $storeId + * @return array + */ + private function searchByBoolAttribute(int $value, int $storeId): array + { + $index = $this->searchIndexNameResolver->getIndexName($storeId, $this->indexer->getId()); + $searchQuery = [ + 'index' => $index, + 'type' => $this->entityType, + 'body' => [ + 'query' => [ + 'query_string' => [ + 'query' => $value, + 'default_field' => 'boolean_attribute', + ], + ], + ], + ]; + $queryResult = $this->client->query($searchQuery); + return isset($queryResult['hits']['hits']) ? $queryResult['hits']['hits'] : []; + } + /** * Returns installed on server search service * diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php index cf87be7e8d710..c6989c7805b4a 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer.php @@ -4,14 +4,27 @@ * See COPYING.txt for license details. */ -/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\App\MutableScopeConfig; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute.php'); -/** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ -$storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); +/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ +$objectManager = Bootstrap::getObjectManager(); -/** @var \Magento\Store\Model\Store $store */ -$store = $objectManager->create(\Magento\Store\Model\Store::class); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var Store $store */ +$store = $objectManager->create(Store::class); $storeCode = 'secondary'; if (!$store->load($storeCode)->getId()) { @@ -23,92 +36,118 @@ ->setIsActive(1); $store->save(); - /** @var \Magento\Framework\App\MutableScopeConfig $scopeConfig */ - $scopeConfig = $objectManager->get(\Magento\Framework\App\MutableScopeConfig::class); + /** @var MutableScopeConfig $scopeConfig */ + $scopeConfig = $objectManager->get(MutableScopeConfig::class); $scopeConfig->setValue( 'general/locale/code', 'de_DE', - \Magento\Store\Model\ScopeInterface::SCOPE_STORES, + ScopeInterface::SCOPE_STORES, $store->getId() ); } -/** @var $productFirst \Magento\Catalog\Model\Product */ -$productFirst = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productFirst->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Apple') - ->setSku('fulltext-1') - ->setPrice(10) - ->setMetaTitle('first meta title') - ->setMetaKeyword('first meta keyword') - ->setMetaDescription('first meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + $productRepository->get('fulltext-1'); +} catch (NoSuchEntityException $e) { + /** @var $productFirst Product */ + $productFirst = $objectManager->create(Product::class); + $productFirst->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Apple') + ->setSku('fulltext-1') + ->setPrice(10) + ->setMetaTitle('first meta title') + ->setMetaKeyword('first meta keyword') + ->setMetaDescription('first meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} -/** @var $productSecond \Magento\Catalog\Model\Product */ -$productSecond = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productSecond->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Banana') - ->setSku('fulltext-2') - ->setPrice(20) - ->setMetaTitle('second meta title') - ->setMetaKeyword('second meta keyword') - ->setMetaDescription('second meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +try { + $productRepository->get('fulltext-2'); +} catch (NoSuchEntityException $e) { + /** @var $productSecond Product */ + $productSecond = $objectManager->create(Product::class); + $productSecond->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Banana') + ->setSku('fulltext-2') + ->setPrice(20) + ->setMetaTitle('second meta title') + ->setMetaKeyword('second meta keyword') + ->setMetaDescription('second meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} -/** @var $productThird \Magento\Catalog\Model\Product */ -$productThird = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productThird->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Orange') - ->setSku('fulltext-3') - ->setPrice(20) - ->setMetaTitle('third meta title') - ->setMetaKeyword('third meta keyword') - ->setMetaDescription('third meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +try { + $productRepository->get('fulltext-3'); +} catch (NoSuchEntityException $e) { + /** @var $productThird Product */ + $productThird = $objectManager->create(Product::class); + $productThird->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Orange') + ->setSku('fulltext-3') + ->setPrice(20) + ->setMetaTitle('third meta title') + ->setMetaKeyword('third meta keyword') + ->setMetaDescription('third meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} -/** @var $productFourth \Magento\Catalog\Model\Product */ -$productFourth = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productFourth->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Papaya') - ->setSku('fulltext-4') - ->setPrice(20) - ->setMetaTitle('fourth meta title') - ->setMetaKeyword('fourth meta keyword') - ->setMetaDescription('fourth meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +try { + $productRepository->get('fulltext-4'); +} catch (NoSuchEntityException $e) { + /** @var $productFourth Product */ + $productFourth = $objectManager->create(Product::class); + $productFourth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Papaya') + ->setSku('fulltext-4') + ->setPrice(20) + ->setMetaTitle('fourth meta title') + ->setMetaKeyword('fourth meta keyword') + ->setMetaDescription('fourth meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} -/** @var $productFifth \Magento\Catalog\Model\Product */ -$productFifth = $objectManager->create(\Magento\Catalog\Model\Product::class); -$productFifth->setTypeId('simple') - ->setAttributeSetId(4) - ->setWebsiteIds([1]) - ->setName('Simple Product Cherry') - ->setSku('fulltext-5') - ->setPrice(20) - ->setMetaTitle('fifth meta title') - ->setMetaKeyword('fifth meta keyword') - ->setMetaDescription('fifth meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 0]) - ->save(); +try { + $productRepository->get('fulltext-5'); +} catch (NoSuchEntityException $e) { + /** @var $productFifth Product */ + $productFifth = $objectManager->create(Product::class); + $productFifth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product Cherry') + ->setSku('fulltext-5') + ->setPrice(20) + ->setMetaTitle('fifth meta title') + ->setMetaKeyword('fifth meta keyword') + ->setMetaDescription('fifth meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/select_attribute_store_labels.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/select_attribute_store_labels.php new file mode 100644 index 0000000000000..cae79a57f3afc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/select_attribute_store_labels.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** + * @var StoreManagerInterface $storeManager + */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$option = [ + 'value' => [ + 'chair' => ['Chair'], + 'table' => ['Table'], + ], + 'order' => [ + 'chair' => 1, + 'table' => 2, + ], +]; + +foreach ($option['value'] as $value => $labels) { + foreach ($storeManager->getStores() as $store) { + $labels[$store->getId()] = $labels[0] . '_' . $store->getCode(); + } + $option['value'][$value] = $labels; +} + +/** @var \Magento\Catalog\Setup\CategorySetup $installer */ +$installer = $objectManager->create(\Magento\Catalog\Setup\CategorySetup::class); + +/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ +$selectAttribute = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +$selectAttribute->setData( + [ + 'attribute_code' => 'select_attribute', + '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' => 0, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Select Attribute'], + 'backend_type' => 'varchar', + 'backend_model' => \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend::class, + 'option' => $option, + ] +); +$selectAttribute->save(); + +$installer->addAttributeToGroup( + 'catalog_product', + 'Default', + 'General', + $selectAttribute->getId() +); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/select_attribute_store_labels_rollback.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/select_attribute_store_labels_rollback.php new file mode 100644 index 0000000000000..93faba2e44884 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/select_attribute_store_labels_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/Elasticsearch/_files/select_attribute_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample index 74c1522fa41f0..930c439899f03 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample @@ -21,11 +21,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicChildMethod(\Laminas\Code\Generator\ClassGenerator $classGenerator, $param1 = '', $param2 = '\\', $param3 = '\'', array $array = []) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicChildMethod'); - if (!$pluginInfo) { - return parent::publicChildMethod($classGenerator, $param1, $param2, $param3, $array); - } else { - return $this->___callPlugins('publicChildMethod', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicChildMethod', func_get_args(), $pluginInfo) : parent::publicChildMethod($classGenerator, $param1, $param2, $param3, $array); } /** @@ -34,11 +30,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicMethodWithReference(\Laminas\Code\Generator\ClassGenerator &$classGenerator, &$param1, array &$array) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicMethodWithReference'); - if (!$pluginInfo) { - return parent::publicMethodWithReference($classGenerator, $param1, $array); - } else { - return $this->___callPlugins('publicMethodWithReference', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicMethodWithReference', func_get_args(), $pluginInfo) : parent::publicMethodWithReference($classGenerator, $param1, $array); } /** @@ -47,11 +39,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicChildWithoutParameters() { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicChildWithoutParameters'); - if (!$pluginInfo) { - return parent::publicChildWithoutParameters(); - } else { - return $this->___callPlugins('publicChildWithoutParameters', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicChildWithoutParameters', func_get_args(), $pluginInfo) : parent::publicChildWithoutParameters(); } /** @@ -60,11 +48,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function public71($arg1, string $arg2, ?int $arg3, ?int $arg4 = null) : void { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'public71'); - if (!$pluginInfo) { - parent::public71($arg1, $arg2, $arg3, $arg4); - } else { - $this->___callPlugins('public71', func_get_args(), $pluginInfo); - } + $pluginInfo ? $this->___callPlugins('public71', func_get_args(), $pluginInfo) : parent::public71($arg1, $arg2, $arg3, $arg4); } /** @@ -73,11 +57,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function public71Another(?\DateTime $arg1, $arg2 = false) : ?string { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'public71Another'); - if (!$pluginInfo) { - return parent::public71Another($arg1, $arg2); - } else { - return $this->___callPlugins('public71Another', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('public71Another', func_get_args(), $pluginInfo) : parent::public71Another($arg1, $arg2); } /** @@ -86,11 +66,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicWithSelf($arg = false) : \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespace { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicWithSelf'); - if (!$pluginInfo) { - return parent::publicWithSelf($arg); - } else { - return $this->___callPlugins('publicWithSelf', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicWithSelf', func_get_args(), $pluginInfo) : parent::publicWithSelf($arg); } /** @@ -99,11 +75,7 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicParentMethod(\Laminas\Code\Generator\DocBlockGenerator $docBlockGenerator, $param1 = '', $param2 = '\\', $param3 = '\'', array $array = []) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicParentMethod'); - if (!$pluginInfo) { - return parent::publicParentMethod($docBlockGenerator, $param1, $param2, $param3, $array); - } else { - return $this->___callPlugins('publicParentMethod', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicParentMethod', func_get_args(), $pluginInfo) : parent::publicParentMethod($docBlockGenerator, $param1, $param2, $param3, $array); } /** @@ -112,10 +84,6 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN public function publicParentWithoutParameters() { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicParentWithoutParameters'); - if (!$pluginInfo) { - return parent::publicParentWithoutParameters(); - } else { - return $this->___callPlugins('publicParentWithoutParameters', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('publicParentWithoutParameters', func_get_args(), $pluginInfo) : parent::publicParentWithoutParameters(); } } 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/customer/order_with_message.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message.php new file mode 100644 index 0000000000000..55b38d9900acd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\GiftMessage\Model\Message; +use Magento\GiftMessage\Model\ResourceModel\Message as MessageResource; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address as OrderAddress; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$addressData = include __DIR__ . '/../../../../Magento/Sales/_files/address_data.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Order $order */ +/** @var Order\Payment $payment */ +/** @var Order\Item $orderItem */ +/** @var array $addressData Data for creating addresses for the orders. */ +$orders = [ + [ + 'increment_id' => '999999990', + 'state' => Order::STATE_NEW, + 'status' => 'processing', + 'grand_total' => 120.00, + 'subtotal' => 120.00, + 'base_grand_total' => 120.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '999999991', + 'state' => Order::STATE_PROCESSING, + 'status' => 'processing', + 'grand_total' => 130.00, + 'base_grand_total' => 130.00, + 'subtotal' => 130.00, + 'total_paid' => 130.00, + 'store_id' => 1, + 'website_id' => 1, + ] +]; + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); + +/** @var array $orderData */ +foreach ($orders as $orderData) { + /** @var Magento\Sales\Model\Order $order */ + $order = $objectManager->create(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 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); + + /** @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'); + + $order + ->setData($orderData) + ->addItem($orderItem) + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setPayment($payment); + $order->setGiftMessageId($messageModel->getId()); + $orderRepository->save($order); +} diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message_rollback.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message_rollback.php new file mode 100644 index 0000000000000..5aaf728243729 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/customer/order_with_message_rollback.php @@ -0,0 +1,46 @@ +<?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\Registry; +use Magento\GiftMessage\Model\Message; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +$productRepository->delete($product); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->create(OrderInterfaceFactory::class); +$orders = []; +$orders[] = $orderFactory->create()->loadByIncrementId('999999990'); +$orders[] = $orderFactory->create()->loadByIncrementId('999999991'); + +foreach ($orders as $order) { + if ($order->getGiftMessageId()) { + $message = $objectManager->create(Message::class); + $message->load($order->getGiftMessageId()); + $message->delete(); + } + if ($order->getId()) { + $orderRepository->delete($order); + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); 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/Newsletter/_files/guest_subscriber.php b/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber.php new file mode 100644 index 0000000000000..e10dcd5985a2e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Newsletter\Model\Subscriber; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +$storeId = $objectManager->get(StoreManagerInterface::class) + ->getStore() + ->getId(); + +/** @var Subscriber $subscriber */ +$subscriber = $objectManager->create(Subscriber::class); + +$subscriber->setStoreId($storeId) + ->setSubscriberEmail('guest@example.com') + ->setSubscriberStatus(Subscriber::STATUS_SUBSCRIBED) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber_rollback.php b/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber_rollback.php new file mode 100644 index 0000000000000..225f6515b5ce7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/_files/guest_subscriber_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Newsletter\Model\Subscriber; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$storeId = $objectManager->get(StoreManagerInterface::class) + ->getStore() + ->getId(); + +/** @var Subscriber $subscriber */ +$subscriber = $objectManager->get(Subscriber::class); +$subscriber->loadBySubscriberEmail('guest@example.com', (int)$storeId); +if ($subscriber->getId()) { + $subscriber->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Payflow/TransparentTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Payflow/TransparentTest.php index eb0976a696300..3f24aaaa8686e 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Payflow/TransparentTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Payflow/TransparentTest.php @@ -47,14 +47,14 @@ protected function setUp(): void } /** - * Checks a case when order should be placed in "Suspected Fraud" status based on after account verification. + * Checks a case when order should be placed in "Suspected Fraud" status based on account verification. * * @magentoDataFixture Magento/Checkout/_files/quote_with_shipping_method.php * @magentoConfigFixture current_store payment/payflowpro/active 1 * @magentoConfigFixture current_store payment/payflowpro/payment_action Authorization * @magentoConfigFixture current_store payment/payflowpro/fmf 1 */ - public function testPlaceOrderSuspectedFraud() + public function testPlaceOrderSuspectedFraud(): void { $quote = $this->getQuote('test_order_1'); $this->addFraudPayment($quote); @@ -114,7 +114,7 @@ private function getQuote(string $reservedOrderId): CartInterface * * @return void */ - private function addFraudPayment(CartInterface $quote) + private function addFraudPayment(CartInterface $quote): void { $payment = $quote->getPayment(); $payment->setMethod(Config::METHOD_PAYFLOWPRO); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/PayflowproVoidTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/PayflowproVoidTest.php new file mode 100644 index 0000000000000..dc1c97e593fae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/PayflowproVoidTest.php @@ -0,0 +1,268 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Model; + +use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Model\Context; +use Magento\Framework\Module\ModuleListInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Payment\Gateway\Command\CommandException; +use Magento\Payment\Helper\Data; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\Method\ConfigInterfaceFactory; +use Magento\Payment\Model\Method\Logger; +use Magento\Paypal\Model\Payflow\Service\Gateway; +use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface; +use Magento\Quote\Api\Data\PaymentMethodInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Item; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PayflowproVoidTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Tests PayflowPro payment void operation. + * + * @magentoDataFixture Magento/Paypal/_files/order_payflowpro.php + * @magentoConfigFixture current_store payment/payflowpro/active 1 + */ + public function testPaymentVoid(): void + { + $response = new DataObject( + [ + 'result' => '0', + 'pnref' => 'V19A3D27B61E', + 'respmsg' => 'Approved', + 'authcode' => '510PNI', + 'hostcode' => 'A', + 'request_id' => 'f930d3dc6824c1f7230c5529dc37ae5e', + 'result_code' => '0', + ] + ); + + $order = $this->getOrder(); + $payment = $order->getPayment(); + $instance = $this->getPaymentMethodInstance($response); + $payment->setMethodInstance($instance); + + $this->assertTrue($order->canVoidPayment()); + + $payment->void(new DataObject()); + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + $orderRepository->save($order); + + $order = $this->getOrderByIncrementId('100000001'); + $this->assertFalse($order->canVoidPayment()); + } + + /** + * Tests canceling order with acceptable void transaction results. + * + * @param DataObject $response + * @magentoDataFixture Magento/Paypal/_files/order_payflowpro.php + * @magentoConfigFixture current_store payment/payflowpro/active 1 + * @dataProvider orderCancelSuccessDataProvider + */ + public function testOrderCancelSuccess(DataObject $response): void + { + $order = $this->getOrder(); + $payment = $order->getPayment(); + $instance = $this->getPaymentMethodInstance($response); + $payment->setMethodInstance($instance); + $order->cancel(); + + $this->assertEquals(Order::STATE_CANCELED, $order->getState()); + $this->assertEquals(Order::STATE_CANCELED, $order->getStatus()); + } + + /** + * @return array + */ + public function orderCancelSuccessDataProvider(): array + { + return [ + 'Authorization has expired' => [ + new DataObject( + [ + 'respmsg' => 'Declined: 10601-Authorization has expired.', + 'result_code' => '10601', + ] + ) + ], + 'Authorization voided successfully' => [ + new DataObject( + [ + 'respmsg' => 'Approved', + 'result_code' => '0', + ] + ) + ] + ]; + } + + /** + * Tests canceling the order when got an error during transaction voiding. + * + * @magentoDataFixture Magento/Paypal/_files/order_payflowpro.php + * @magentoConfigFixture current_store payment/payflowpro/active 1 + */ + public function testOrderCancelWithVoidError(): void + { + $response = new DataObject( + [ + 'respmsg' => 'Declined: for some reason other then expired authorization', + 'result_code' => '111', + ] + ); + $order = $this->getOrder(); + $payment = $order->getPayment(); + $instance = $this->getPaymentMethodInstance($response); + $payment->setMethodInstance($instance); + + $this->expectException(CommandException::class); + $order->cancel(); + } + + /** + * Returns prepared order. + * + * @return Order + * @throws \ReflectionException + */ + private function getOrder(): Order + { + /** @var $order Order */ + $order = $this->getOrderByIncrementId('100000001'); + $orderItem = $this->createMock(Item::class); + $orderItem->method('getQtyToInvoice') + ->willReturn(true); + $order->setItems([$orderItem]); + + $payment = $order->getPayment(); + $canVoidLookupProperty = new \ReflectionProperty(get_class($payment), '_canVoidLookup'); + $canVoidLookupProperty->setAccessible(true); + $canVoidLookupProperty->setValue($payment, true); + + return $order; + } + + /** + * Returns payment method instance. + * + * @param DataObject $response + * @return PaymentMethodInterface + * @throws \ReflectionException + */ + private function getPaymentMethodInstance(DataObject $response): PaymentMethodInterface + { + $gatewayMock = $this->createMock(Gateway::class); + $gatewayMock->expects($this->once()) + ->method('postRequest') + ->willReturn($response); + + $configMock = $this->createMock(PayflowConfig::class); + $configFactoryMock = $this->createPartialMock( + ConfigInterfaceFactory::class, + ['create'] + ); + + $configFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($configMock); + + $configMock->expects($this->any()) + ->method('getValue') + ->willReturnMap( + [ + ['use_proxy', false], + ['sandbox_flag', '1'], + ['transaction_url_test_mode', 'https://test_transaction_url'] + ] + ); + + /** @var Payflowpro|MockObject $instance */ + $instance = $this->getMockBuilder(Payflowpro::class) + ->setMethods(['setStore', 'getInfoInstance']) + ->setConstructorArgs( + [ + $this->objectManager->get(Context::class), + $this->objectManager->get(Registry::class), + $this->objectManager->get(ExtensionAttributesFactory::class), + $this->objectManager->get(AttributeValueFactory::class), + $this->objectManager->get(Data::class), + $this->objectManager->get(ScopeConfigInterface::class), + $this->objectManager->get(Logger::class), + $this->objectManager->get(ModuleListInterface::class), + $this->objectManager->get(TimezoneInterface::class), + $this->objectManager->get(StoreManagerInterface::class), + $configFactoryMock, + $gatewayMock, + $this->objectManager->get(HandlerInterface::class), + null, + null, + [] + ] + ) + ->getMock(); + + $instance->expects($this->once()) + ->method('setStore') + ->willReturnSelf(); + $paymentInfoInstance = $this->createMock(InfoInterface::class); + $instance->method('getInfoInstance') + ->willReturn($paymentInfoInstance); + + return $instance; + } + + /** + * Get stored order. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrderByIncrementId(string $incrementId): OrderInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementId) + ->create(); + + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + $orders = $orderRepository->getList($searchCriteria) + ->getItems(); + + /** @var OrderInterface $order */ + return array_pop($orders); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/VoidTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/VoidTest.php deleted file mode 100644 index 6f295a62f48fb..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/VoidTest.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Paypal\Model; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class VoidTest extends \PHPUnit\Framework\TestCase -{ - /** - * @magentoDataFixture Magento/Paypal/_files/order_payflowpro.php - * @magentoConfigFixture current_store payment/payflowpro/active 1 - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testPayflowProVoid() - { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** @var $order \Magento\Sales\Model\Order */ - $order = $objectManager->create(\Magento\Sales\Model\Order::class); - $order->loadByIncrementId('100000001'); - $payment = $order->getPayment(); - - $gatewayMock = $this->createMock(\Magento\Paypal\Model\Payflow\Service\Gateway::class); - - $configMock = $this->createMock(\Magento\Paypal\Model\PayflowConfig::class); - $configFactoryMock = $this->createPartialMock( - \Magento\Payment\Model\Method\ConfigInterfaceFactory::class, - ['create'] - ); - - $configFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($configMock); - - $configMock->expects($this->any()) - ->method('getValue') - ->willReturnMap( - [ - ['use_proxy', false], - ['sandbox_flag', '1'], - ['transaction_url_test_mode', 'https://test_transaction_url'] - ] - ); - - /** @var \Magento\Paypal\Model\Payflowpro|\PHPUnit\Framework\MockObject\MockObject $instance */ - $instance = $this->getMockBuilder(\Magento\Paypal\Model\Payflowpro::class) - ->setMethods(['setStore']) - ->setConstructorArgs( - [ - $objectManager->get(\Magento\Framework\Model\Context::class), - $objectManager->get(\Magento\Framework\Registry::class), - $objectManager->get(\Magento\Framework\Api\ExtensionAttributesFactory::class), - $objectManager->get(\Magento\Framework\Api\AttributeValueFactory::class), - $objectManager->get(\Magento\Payment\Helper\Data::class), - $objectManager->get(\Magento\Framework\App\Config\ScopeConfigInterface::class), - $objectManager->get(\Magento\Payment\Model\Method\Logger::class), - $objectManager->get(\Magento\Framework\Module\ModuleListInterface::class), - $objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class), - $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class), - $configFactoryMock, - $gatewayMock, - $objectManager->get(\Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface::class), - null, - null, - [] - ] - ) - ->getMock(); - - $response = new \Magento\Framework\DataObject( - [ - 'result' => '0', - 'pnref' => 'V19A3D27B61E', - 'respmsg' => 'Approved', - 'authcode' => '510PNI', - 'hostcode' => 'A', - 'request_id' => 'f930d3dc6824c1f7230c5529dc37ae5e', - 'result_code' => '0', - ] - ); - - $gatewayMock->expects($this->once()) - ->method('postRequest') - ->willReturn($response); - $instance->expects($this->once()) - ->method('setStore') - ->willReturnSelf(); - - $payment->setMethodInstance($instance); - $payment->void(new \Magento\Framework\DataObject()); - $order->save(); - - $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Sales\Model\Order::class); - $order->loadByIncrementId('100000001'); - $this->assertFalse($order->canVoidPayment()); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/RemoveQuoteItemsTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/RemoveQuoteItemsTest.php new file mode 100644 index 0000000000000..ccc146c459b07 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/RemoveQuoteItemsTest.php @@ -0,0 +1,75 @@ +<?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\ResourceModel\Product as ProductResourceModel; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Tests for remove quote items plugin. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class RemoveQuoteItemsTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var ProductResourceModel */ + private $productResoure; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->productResoure = $this->objectManager->get(ProductResourceModel::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + } + + /** + * @return void + */ + public function testPluginIsRegistered(): void + { + $pluginInfo = $this->objectManager->get(PluginList::class)->get(ProductResourceModel::class); + $this->assertSame( + RemoveQuoteItems::class, + $pluginInfo['clean_quote_items_after_product_delete']['instance'] + ); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * + * @return void + */ + public function testDeleteProduct(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->assertNotNull($quote); + $quoteItems = $quote->getItems(); + $quoteItem = current($quoteItems); + $this->assertNotNull($quoteItem); + $this->productResoure->delete($quoteItem->getProduct()); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->assertNotNull($quote); + $this->assertEmpty($quote->getItems()); + } +} 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/Block/Order/Item/Renderer/DefaultRendererTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/Item/Renderer/DefaultRendererTest.php index 2543313d6fdce..057f75874032d 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/Item/Renderer/DefaultRendererTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/Item/Renderer/DefaultRendererTest.php @@ -8,7 +8,11 @@ namespace Magento\Sales\Block\Order\Item\Renderer; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderInterfaceFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; @@ -19,6 +23,7 @@ * * @magentoAppArea frontend * @magentoDbIsolation enabled + * @magentoAppIsolation enabled */ class DefaultRendererTest extends TestCase { @@ -31,14 +36,43 @@ class DefaultRendererTest extends TestCase /** @var OrderInterfaceFactory */ private $orderFactory; + /** @var PageFactory */ + private $pageFactory; + + /** @var Registry */ + private $registry; + + /** + * @var array + */ + private $defaultFieldsToCheck = [ + 'name' => "//td[contains(@class, 'name')]/strong[contains(text(), '%s')]", + 'sku' => "//td[contains(@class, 'sku') and contains(text(), '%s')]", + 'qty' => "//td[contains(@class, 'qty') and contains(text(), '%d')]", + ]; + /** * @inheritdoc */ protected function setUp(): void { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(DefaultRenderer::class); $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_order'); + + parent::tearDown(); } /** @@ -55,43 +89,13 @@ public function testDisplayingShipmentItem(): void $this->assertNotNull($item); $blockHtml = $this->block->setTemplate('Magento_Sales::order/shipment/items/renderer/default.phtml') ->setItem($item)->toHtml(); - $this->assertEquals( - 1, - Xpath::getElementsCountForXpath( - sprintf( - "//td[contains(@class, 'name')]/strong[contains(text(), '%s')]", - $item->getName() - ), - $blockHtml - ), - sprintf('Item with name %s wasn\'t found.', $item->getName()) - ); - $this->assertEquals( - 1, - Xpath::getElementsCountForXpath( - sprintf( - "//td[contains(@class, 'sku') and contains(text(), '%s')]", - $item->getSku() - ), - $blockHtml - ), - sprintf('Item with sku %s wasn\'t found.', $item->getSku()) - ); - $this->assertEquals( - 1, - Xpath::getElementsCountForXpath( - sprintf( - "//td[contains(@class, 'qty') and contains(text(), '%d')]", - $item->getQty() - ), - $blockHtml - ), - sprintf( - 'Qty for item %s wasn\'t found or not equals to %s.', - $item->getName(), - $item->getQty() - ) - ); + foreach ($this->defaultFieldsToCheck as $key => $xpath) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($xpath, $item->getData($key)), $blockHtml), + sprintf('Item %s wasn\'t found or not equals to %s.', $key, $item->getData($key)) + ); + } } /** @@ -108,4 +112,161 @@ public function testCreditmemoItemTotalAmount(): void $this->assertNotNull($item->getId()); $this->assertEquals(10.00, $this->block->getTotalAmount($item)); } + + /** + * @magentoDataFixture Magento/Sales/_files/customer_order_with_two_items.php + * + * @return void + */ + public function testPrintOrderItem(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->registerOrder($order); + $item = $order->getItemsCollection()->getFirstItem(); + $this->assertNotNull($item->getId()); + $block = $this->getBlock('sales_order_print', 'sales.order.print.renderers.default'); + $this->assertNotFalse($block); + $blockHtml = $block->setItem($item)->toHtml(); + $fieldsToCheck = [ + 'name' => "//td[contains(@class, 'name')]/strong[contains(text(), '%s')]", + 'sku' => "//td[contains(@class, 'sku') and contains(text(), '%s')]", + 'price' => "//td[contains(@class, 'price')]//span[contains(text(), '%01.2f')]", + 'qty_ordered' => "//td[contains(@class, 'qty')]//span[contains(text(), '" . __('Ordered') + . "')]/following-sibling::span[contains(text(), '%d')]", + 'row_total' => "//td[contains(@class, 'subtotal')]//span[contains(text(), '%01.2f')]", + ]; + foreach ($fieldsToCheck as $key => $xpath) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($xpath, $item->getData($key)), $blockHtml), + sprintf('Item %s wasn\'t found or not equals to %s.', $key, $item->getData($key)) + ); + } + } + + /** + * @magentoDataFixture Magento/Sales/_files/invoices_for_items.php + * + * @return void + */ + public function testPrintInvoiceItem(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->registerOrder($order); + $invoice = $order->getInvoiceCollection()->getFirstItem(); + $this->assertNotNull($invoice->getId()); + $item = $invoice->getItemsCollection()->getFirstItem(); + $this->assertNotNull($item->getId()); + $block = $this->getBlock('sales_order_printinvoice', 'sales.order.print.invoice.renderers.default'); + $this->assertNotFalse($block); + $blockHtml = $block->setItem($item)->toHtml(); + $additionalFields = [ + 'price' => "//td[contains(@class, 'price')]//span[contains(text(), '%01.2f')]", + 'qty' => "//td[contains(@class, 'qty')]/span[contains(text(), '%d')]", + 'row_total' => "//td[contains(@class, 'subtotal')]//span[contains(text(), '%01.2f')]", + ]; + $this->defaultFieldsToCheck = array_merge($this->defaultFieldsToCheck, $additionalFields); + foreach ($this->defaultFieldsToCheck as $key => $xpath) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($xpath, $item->getData($key)), $blockHtml), + sprintf('Item %s wasn\'t found or not equals to %s.', $key, $item->getData($key)) + ); + } + } + + /** + * @magentoDataFixture Magento/Sales/_files/shipment_for_order_with_customer.php + * + * @return void + */ + public function testPrintShipmentItem(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->registerOrder($order); + $shipment = $order->getShipmentsCollection()->getFirstItem(); + $this->assertNotNull($shipment->getId()); + $item = $shipment->getAllItems()[0] ?? null; + $this->assertNotNull($item); + $block = $this->getBlock('sales_order_printshipment', 'sales.order.print.shipment.renderers.default'); + $this->assertNotFalse($block); + $blockHtml = $block->setItem($item)->toHtml(); + foreach ($this->defaultFieldsToCheck as $key => $xpath) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($xpath, $item->getData($key)), $blockHtml), + sprintf('Item %s wasn\'t found or not equals to %s.', $key, $item->getData($key)) + ); + } + } + + /** + * @magentoDataFixture Magento/Sales/_files/refunds_for_items.php + * + * @return void + */ + public function testPrintCreditmemoItem(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->registerOrder($order); + $creditmemo = $order->getCreditmemosCollection()->getFirstItem(); + $this->assertNotNull($creditmemo->getId()); + $item = $creditmemo->getItemsCollection()->getFirstItem(); + $this->assertNotNull($item->getId()); + $block = $this->getBlock('sales_order_printcreditmemo', 'sales.order.print.creditmemo.renderers.default'); + $this->assertNotFalse($block); + $blockHtml = $block->setItem($item)->toHtml(); + $additionalFields = [ + 'price' => "//td[contains(@class, 'price')]//span[contains(text(), '%01.2f')]", + 'row_total' => "//td[contains(@class, 'subtotal')]//span[contains(text(), '%01.2f')]", + 'discount_amount' => "//td[contains(@class, 'discount')]/span[contains(text(), '%01.2f')]", + ]; + $this->defaultFieldsToCheck = array_merge($this->defaultFieldsToCheck, $additionalFields); + foreach ($this->defaultFieldsToCheck as $key => $xpath) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($xpath, $item->getData($key)), $blockHtml), + sprintf('Item %s wasn\'t found or not equals to %s.', $key, $item->getData($key)) + ); + } + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//td[contains(@class, 'total')]/span[contains(text(), '%01.2f')]", + $this->block->getTotalAmount($item) + ), + $blockHtml + ), + sprintf('Item total wasn\'t found or not equals to %s.', $this->block->getTotalAmount($item)) + ); + } + + /** + * Get block. + * + * @param string $handle + * @param string $blockName + * @return AbstractBlock + */ + private function getBlock(string $handle, string $blockName): AbstractBlock + { + $page = $this->pageFactory->create(); + $page->addHandle(['default', $handle]); + $page->getLayout()->generateXml(); + + return $page->getLayout()->getBlock($blockName); + } + + /** + * Register order in registry. + * + * @param OrderInterface $order + * @return void + */ + private function registerOrder(OrderInterface $order): void + { + $this->registry->unregister('current_order'); + $this->registry->register('current_order', $order); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/CreditmemoTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/CreditmemoTest.php index d5ca29cd0f0b7..c50c1b23c3f80 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/CreditmemoTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/CreditmemoTest.php @@ -3,41 +3,268 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Order\PrintOrder; -class CreditmemoTest extends \PHPUnit\Framework\TestCase +use Magento\Directory\Model\CountryFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Element\Text; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Api\Data\CreditmemoInterfaceFactory; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\Data\OrderPaymentInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Tests for print creditmemo block. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreditmemoTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + /** @var LayoutInterface */ + private $layout; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var CreditmemoInterfaceFactory */ + private $creditmemoFactory; + + /** @var PageFactory */ + private $pageFactory; + + /** @var CountryFactory */ + private $countryFactory; + + /** @var OrderPaymentInterfaceFactory */ + private $orderPaymentFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->creditmemoFactory = $this->objectManager->get(CreditmemoInterfaceFactory::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->countryFactory = $this->objectManager->get(CountryFactory::class); + $this->orderPaymentFactory = $this->objectManager->create(OrderPaymentInterfaceFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_order'); + $this->registry->unregister('current_creditmemo'); + + parent::tearDown(); + } + /** * @magentoAppIsolation enabled + * + * @return void */ - public function testGetTotalsHtml() + public function testGetTotalsHtml(): void { - $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Sales\Model\Order::class); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $objectManager->get(\Magento\Framework\Registry::class)->register('current_order', $order); - $payment = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order\Payment::class - ); + $order = $this->orderFactory->create(); + $this->registerOrder($order); + $payment = $this->orderPaymentFactory->create(); $payment->setMethod('checkmo'); $order->setPayment($payment); - - $layout = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - ); - $block = $layout->createBlock(\Magento\Sales\Block\Order\PrintOrder\Creditmemo::class, 'block'); - $childBlock = $layout->addBlock(\Magento\Framework\View\Element\Text::class, 'creditmemo_totals', 'block'); - + $block = $this->layout->createBlock(Creditmemo::class, 'block'); + $childBlock = $this->layout->addBlock(Text::class, 'creditmemo_totals', 'block'); $expectedHtml = '<b>Any html</b>'; - $creditmemo = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order\Creditmemo::class - ); + $creditmemo = $this->creditmemoFactory->create(); $this->assertEmpty($childBlock->getCreditmemo()); $this->assertNotEquals($expectedHtml, $block->getTotalsHtml($creditmemo)); - $childBlock->setText($expectedHtml); $actualHtml = $block->getTotalsHtml($creditmemo); $this->assertSame($creditmemo, $childBlock->getCreditmemo()); $this->assertEquals($expectedHtml, $actualHtml); } + + /** + * @magentoDataFixture Magento/Sales/_files/refunds_for_items.php + * + * @return void + */ + public function testPrintCreditmemo(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $creditmemo = $order->getCreditmemosCollection()->getFirstItem(); + $this->assertNotNull($creditmemo->getId()); + $this->registerOrder($order); + $this->registerCreditmemo($creditmemo); + $blockHtml = $this->renderPrintCreditmemoBlock(); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'order-title')]/strong[contains(text(), '%s')]", + __('Refund #%1', $creditmemo->getIncrementId()) + ), + $blockHtml + ), + sprintf('Title for %s was not found.', __('Refund #%1', $creditmemo->getIncrementId())) + ); + $this->assertOrderInformation($order, $blockHtml); + } + + /** + * @magentoDataFixture Magento/Sales/_files/refunds_for_items.php + * + * @return void + */ + public function testOrderInformation(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->registerOrder($order); + $block = $this->layout->createBlock(Creditmemo::class); + $orderDate = $block->formatDate($order->getCreatedAt(), \IntlDateFormatter::LONG); + $templates = [ + 'Order status' => [ + 'template' => 'Magento_Sales::order/order_status.phtml', + 'expected_data' => (string)__($order->getStatusLabel()), + ], + 'Order date' => [ + 'template' => 'Magento_Sales::order/order_date.phtml', + 'expected_data' => (string)__('Order Date: %1', $orderDate), + ], + ]; + foreach ($templates as $key => $data) { + $this->assertStringContainsString( + $data['expected_data'], + strip_tags($block->setTemplate($data['template'])->toHtml()), + sprintf('%s wasn\'t found.', $key) + ); + } + } + + /** + * Assert order information block. + * + * @param OrderInterface $order + * @param string $html + * @return void + */ + private function assertOrderInformation(OrderInterface $order, string $html): void + { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + "//div[contains(@class, 'block-order-details-view')]" + . "//strong[contains(text(), '" . __('Order Information') . "')]", + $html + ), + __('Order Information') . ' title wasn\'t found.' + ); + foreach ([$order->getShippingAddress(), $order->getBillingAddress()] as $address) { + $addressBoxXpath = sprintf("//div[contains(@class, 'box-order-%s-address')]", $address->getAddressType()) + . "//address[contains(., '%s')]"; + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($addressBoxXpath, $address->getName()), $html), + sprintf('Customer name for %s address wasn\'t found.', $address->getAddressType()) + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + $addressBoxXpath, + $this->countryFactory->create()->loadByCode($address->getData('country_id'))->getName() + ), + $html + ), + sprintf('Country for %s address wasn\'t found.', $address->getAddressType()) + ); + $attributes = ['company', 'street', 'city', 'region', 'postcode', 'telephone']; + foreach ($attributes as $key) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($addressBoxXpath, $address->getData($key)), $html), + sprintf('%s for %s address wasn\'t found.', $key, $address->getAddressType()) + ); + } + } + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'box-order-shipping-method') and contains(.//strong, '%s')]" + . "//div[contains(text(), '%s')]", + __('Shipping Method'), + $order->getShippingDescription() + ), + $html + ), + 'Shipping method for order wasn\'t found.' + ); + } + + /** + * Register order in registry. + * + * @param OrderInterface $order + * @return void + */ + private function registerOrder(OrderInterface $order): void + { + $this->registry->unregister('current_order'); + $this->registry->register('current_order', $order); + } + + /** + * Register creditmemo in registry. + * + * @param CreditmemoInterface $creditmemo + * @return void + */ + private function registerCreditmemo(CreditmemoInterface $creditmemo): void + { + $this->registry->unregister('current_creditmemo'); + $this->registry->register('current_creditmemo', $creditmemo); + } + + /** + * Render print creditmemo block. + * + * @return string + */ + private function renderPrintCreditmemoBlock(): string + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'sales_order_printcreditmemo', + ]); + $page->getLayout()->generateXml(); + $printCreditmemoBlock = $page->getLayout()->getBlock('sales.order.print.creditmemo'); + $this->assertNotFalse($printCreditmemoBlock); + + return $printCreditmemoBlock->toHtml(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/InvoiceTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/InvoiceTest.php index 2ee19b1188075..5bdd9aa0f3d1c 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/InvoiceTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/InvoiceTest.php @@ -3,41 +3,268 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Order\PrintOrder; -class InvoiceTest extends \PHPUnit\Framework\TestCase +use Magento\Directory\Model\CountryFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Element\Text; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceInterfaceFactory; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\Data\OrderPaymentInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Tests for print invoice block. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class InvoiceTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + /** @var LayoutInterface */ + private $layout; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var InvoiceInterfaceFactory */ + private $invoiceFactory; + + /** @var PageFactory */ + private $pageFactory; + + /** @var CountryFactory */ + private $countryFactory; + + /** @var OrderPaymentInterfaceFactory */ + private $orderPaymentFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->invoiceFactory = $this->objectManager->get(InvoiceInterfaceFactory::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->countryFactory = $this->objectManager->get(CountryFactory::class); + $this->orderPaymentFactory = $this->objectManager->create(OrderPaymentInterfaceFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_order'); + $this->registry->unregister('current_invoice'); + + parent::tearDown(); + } + /** * @magentoAppIsolation enabled + * + * @return void */ - public function testGetInvoiceTotalsHtml() + public function testGetInvoiceTotalsHtml(): void { - $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Sales\Model\Order::class); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $objectManager->get(\Magento\Framework\Registry::class)->register('current_order', $order); - $payment = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order\Payment::class - ); + $order = $this->orderFactory->create(); + $this->registerOrder($order); + $payment = $this->orderPaymentFactory->create(); $payment->setMethod('checkmo'); $order->setPayment($payment); - - $layout = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - ); - $block = $layout->createBlock(\Magento\Sales\Block\Order\PrintOrder\Invoice::class, 'block'); - $childBlock = $layout->addBlock(\Magento\Framework\View\Element\Text::class, 'invoice_totals', 'block'); - + $block = $this->layout->createBlock(Invoice::class, 'block'); + $childBlock = $this->layout->addBlock(Text::class, 'invoice_totals', 'block'); $expectedHtml = '<b>Any html</b>'; - $invoice = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order\Invoice::class - ); + $invoice = $this->invoiceFactory->create(); $this->assertEmpty($childBlock->getInvoice()); $this->assertNotEquals($expectedHtml, $block->getInvoiceTotalsHtml($invoice)); - $childBlock->setText($expectedHtml); $actualHtml = $block->getInvoiceTotalsHtml($invoice); $this->assertSame($invoice, $childBlock->getInvoice()); $this->assertEquals($expectedHtml, $actualHtml); } + + /** + * @magentoDataFixture Magento/Sales/_files/invoices_for_items.php + * + * @return void + */ + public function testPrintInvoice(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $invoice = $order->getInvoiceCollection()->getFirstItem(); + $this->assertNotNull($invoice->getId()); + $this->registerOrder($order); + $this->registerInvoice($invoice); + $blockHtml = $this->renderPrintInvoiceBlock(); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'order-title')]/strong[contains(text(), '%s')]", + __('Invoice #%1', (int)$invoice->getIncrementId()) + ), + $blockHtml + ), + sprintf('Title for %s was not found.', __('Invoice #%1', (int)$invoice->getIncrementId())) + ); + $this->assertOrderInformation($order, $blockHtml); + } + + /** + * @magentoDataFixture Magento/Sales/_files/invoices_for_items.php + * + * @return void + */ + public function testOrderInformation(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->registerOrder($order); + $block = $this->layout->createBlock(Invoice::class); + $orderDate = $block->formatDate($order->getCreatedAt(), \IntlDateFormatter::LONG); + $templates = [ + 'Order status' => [ + 'template' => 'Magento_Sales::order/order_status.phtml', + 'expected_data' => (string)__($order->getStatusLabel()), + ], + 'Order date' => [ + 'template' => 'Magento_Sales::order/order_date.phtml', + 'expected_data' => (string)__('Order Date: %1', $orderDate), + ], + ]; + foreach ($templates as $key => $data) { + $this->assertStringContainsString( + $data['expected_data'], + strip_tags($block->setTemplate($data['template'])->toHtml()), + sprintf('%s wasn\'t found.', $key) + ); + } + } + + /** + * Assert order information block. + * + * @param OrderInterface $order + * @param string $html + * @return void + */ + private function assertOrderInformation(OrderInterface $order, string $html): void + { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + "//div[contains(@class, 'block-order-details-view')]" + . "//strong[contains(text(), '" . __('Order Information') . "')]", + $html + ), + __('Order Information') . ' title wasn\'t found.' + ); + foreach ([$order->getShippingAddress(), $order->getBillingAddress()] as $address) { + $addressBoxXpath = sprintf("//div[contains(@class, 'box-order-%s-address')]", $address->getAddressType()) + . "//address[contains(., '%s')]"; + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($addressBoxXpath, $address->getName()), $html), + sprintf('Customer name for %s address wasn\'t found.', $address->getAddressType()) + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + $addressBoxXpath, + $this->countryFactory->create()->loadByCode($address->getData('country_id'))->getName() + ), + $html + ), + sprintf('Country for %s address wasn\'t found.', $address->getAddressType()) + ); + $attributes = ['company', 'street', 'city', 'region', 'postcode', 'telephone']; + foreach ($attributes as $key) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($addressBoxXpath, $address->getData($key)), $html), + sprintf('%s for %s address wasn\'t found.', $key, $address->getAddressType()) + ); + } + } + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'box-order-shipping-method') and contains(.//strong, '%s')]" + . "//div[contains(text(), '%s')]", + __('Shipping Method'), + $order->getShippingDescription() + ), + $html + ), + 'Shipping method for order wasn\'t found.' + ); + } + + /** + * Register order in registry. + * + * @param OrderInterface $order + * @return void + */ + private function registerOrder(OrderInterface $order): void + { + $this->registry->unregister('current_order'); + $this->registry->register('current_order', $order); + } + + /** + * Register invoice in registry. + * + * @param InvoiceInterface $invoice + * @return void + */ + private function registerInvoice(InvoiceInterface $invoice): void + { + $this->registry->unregister('current_invoice'); + $this->registry->register('current_invoice', $invoice); + } + + /** + * Render print invoice block. + * + * @return string + */ + private function renderPrintInvoiceBlock(): string + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'sales_order_printinvoice', + ]); + $page->getLayout()->generateXml(); + $printInvoiceBlock = $page->getLayout()->getBlock('sales.order.print.invoice'); + $this->assertNotFalse($printInvoiceBlock); + + return $printInvoiceBlock->toHtml(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/ShipmentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/ShipmentTest.php new file mode 100644 index 0000000000000..434dacec5c6b8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/ShipmentTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Order\PrintOrder; + +use Magento\Directory\Model\CountryFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Tests for print shipment block. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ShipmentTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + /** @var LayoutInterface */ + private $layout; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var PageFactory */ + private $pageFactory; + + /** @var CountryFactory */ + private $countryFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->countryFactory = $this->objectManager->get(CountryFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_order'); + $this->registry->unregister('current_shipment'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/shipment_for_two_items.php + * + * @return void + */ + public function testPrintShipment(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->registerOrder($order); + $shipment = $order->getShipmentsCollection()->getFirstItem(); + $this->assertNotNull($shipment->getId()); + $this->registerOrder($order); + $this->registerShipment($shipment); + $blockHtml = $this->renderPrintShipmentBlock(); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'order-title')]/strong[contains(text(), '%s')]", + __('Shipment #%1', $shipment->getIncrementId()) + ), + $blockHtml + ), + sprintf('Title for %s was not found.', __('Shipment #%1', $shipment->getIncrementId())) + ); + $this->assertOrderInformation($order, $blockHtml); + } + + /** + * @magentoDataFixture Magento/Sales/_files/shipment_for_order_with_customer.php + * + * @return void + */ + public function testOrderInformation(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->registerOrder($order); + $block = $this->layout->createBlock(Shipment::class); + $orderDate = $block->formatDate($order->getCreatedAt(), \IntlDateFormatter::LONG); + $templates = [ + 'Order status' => [ + 'template' => 'Magento_Sales::order/order_status.phtml', + 'expected_data' => (string)__($order->getStatusLabel()), + ], + 'Order date' => [ + 'template' => 'Magento_Sales::order/order_date.phtml', + 'expected_data' => (string)__('Order Date: %1', $orderDate), + ], + ]; + foreach ($templates as $key => $data) { + $this->assertStringContainsString( + $data['expected_data'], + strip_tags($block->setTemplate($data['template'])->toHtml()), + sprintf('%s wasn\'t found.', $key) + ); + } + } + + /** + * Assert order information block. + * + * @param OrderInterface $order + * @param string $html + * @return void + */ + private function assertOrderInformation(OrderInterface $order, string $html): void + { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + "//div[contains(@class, 'block-order-details-view')]" + . "//strong[contains(text(), '" . __('Order Information') . "')]", + $html + ), + __('Order Information') . ' title wasn\'t found.' + ); + foreach ([$order->getShippingAddress(), $order->getBillingAddress()] as $address) { + $addressBoxXpath = ($address->getAddressType() == 'shipping') + ? "//div[contains(@class, 'box-order-shipping-address')]//address[contains(., '%s')]" + : "//div[contains(@class, 'box-order-billing-method')]//address[contains(., '%s')]"; + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($addressBoxXpath, $address->getName()), $html), + sprintf('Customer name for %s address wasn\'t found.', $address->getAddressType()) + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + $addressBoxXpath, + $this->countryFactory->create()->loadByCode($address->getData('country_id'))->getName() + ), + $html + ), + sprintf('Country for %s address wasn\'t found.', $address->getAddressType()) + ); + $attributes = ['company', 'street', 'city', 'region', 'postcode', 'telephone']; + foreach ($attributes as $key) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($addressBoxXpath, $address->getData($key)), $html), + sprintf('%s for %s address wasn\'t found.', $key, $address->getAddressType()) + ); + } + } + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'box-order-shipping-method') and contains(.//strong, '%s')]" + . "//div[contains(text(), '%s')]", + __('Shipping Method'), + $order->getShippingDescription() + ), + $html + ), + 'Shipping method for order wasn\'t found.' + ); + } + + /** + * Register order in registry. + * + * @param OrderInterface $order + * @return void + */ + private function registerOrder(OrderInterface $order): void + { + $this->registry->unregister('current_order'); + $this->registry->register('current_order', $order); + } + + /** + * Register shipment in registry. + * + * @param ShipmentInterface $shipment + * @return void + */ + private function registerShipment(ShipmentInterface $shipment): void + { + $this->registry->unregister('current_shipment'); + $this->registry->register('current_shipment', $shipment); + } + + /** + * Render print shipment block. + * + * @return string + */ + private function renderPrintShipmentBlock(): string + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'sales_order_printshipment', + ]); + $page->getLayout()->generateXml(); + $printShipmentBlock = $page->getLayout()->getBlock('sales.order.print.shipment'); + $this->assertNotFalse($printShipmentBlock); + + return $printShipmentBlock->toHtml(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintShipmentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintShipmentTest.php new file mode 100644 index 0000000000000..5a6dc50bac0c2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintShipmentTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Order; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Layout; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for print shipment block. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class PrintShipmentTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + /** @var Layout */ + private $layout; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/shipment_for_order_with_customer.php + * + * @return void + */ + public function testOrderInformation(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->registerOrder($order); + $block = $this->layout->createBlock(PrintShipment::class); + $orderDate = $block->formatDate($order->getCreatedAt(), \IntlDateFormatter::LONG); + $templates = [ + 'Order status' => [ + 'template' => 'Magento_Sales::order/order_status.phtml', + 'expected_data' => (string)__($order->getStatusLabel()), + ], + 'Order date' => [ + 'template' => 'Magento_Sales::order/order_date.phtml', + 'expected_data' => (string)__('Order Date: %1', $orderDate), + ], + ]; + foreach ($templates as $key => $data) { + $this->assertStringContainsString( + $data['expected_data'], + strip_tags($block->setTemplate($data['template'])->toHtml()), + sprintf('%s wasn\'t found.', $key) + ); + } + } + + /** + * Register order in registry. + * + * @param OrderInterface $order + * @return void + */ + private function registerOrder(OrderInterface $order): void + { + $this->registry->unregister('current_order'); + $this->registry->register('current_order', $order); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Cron/CleanExpiredQuotesTest.php b/dev/tests/integration/testsuite/Magento/Sales/Cron/CleanExpiredQuotesTest.php index f6dee4c7ff0fa..f69c97a635e76 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Cron/CleanExpiredQuotesTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Cron/CleanExpiredQuotesTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Quote\Model\QuoteRepository; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; use Magento\TestFramework\Helper\Bootstrap; /** @@ -25,14 +26,9 @@ class CleanExpiredQuotesTest extends \PHPUnit\Framework\TestCase private $cleanExpiredQuotes; /** - * @var QuoteRepository + * @var QuoteCollectionFactory */ - private $quoteRepository; - - /** - * @var SearchCriteriaBuilder - */ - private $searchCriteriaBuilder; + private $quoteCollectionFactory; /** * @inheritdoc @@ -41,8 +37,7 @@ protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); $this->cleanExpiredQuotes = $objectManager->get(CleanExpiredQuotes::class); - $this->quoteRepository = $objectManager->get(QuoteRepository::class); - $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); } /** @@ -53,17 +48,43 @@ protected function setUp(): void */ public function testExecute() { - $searchCriteria = $this->searchCriteriaBuilder->create(); //Initial count - should be equal to stores number. - $this->assertEquals(2, $this->quoteRepository->getList($searchCriteria)->getTotalCount()); + $this->assertQuotesCount(2); //Deleting expired quotes $this->cleanExpiredQuotes->execute(); - $totalCount = $this->quoteRepository->getList($searchCriteria)->getTotalCount(); + //Only 1 will be deleted for the store that has all of them expired by config (default_store) - $this->assertEquals( - 1, - $totalCount - ); + $this->assertQuotesCount(1); + } + + /** + * Check if outdated quotes are deleted. + * + * @magentoConfigFixture default_store checkout/cart/delete_quote_after -365 + * @magentoDataFixture Magento/Sales/_files/quotes_big_amount.php + */ + public function testExecuteWithBigAmountOfQuotes() + { + //Initial count - should be equal to 1000 + $this->assertQuotesCount(1000); + + //Deleting expired quotes + $this->cleanExpiredQuotes->execute(); + + //There should be no quotes anymore + $this->assertQuotesCount(0); + } + + /** + * Optimized assert quotes count + * Uses collection getSize in order to get quick result + * + * @param int $expected + */ + private function assertQuotesCount(int $expected): void + { + $totalCount = $this->quoteCollectionFactory->create()->getSize(); + $this->assertEquals($expected, $totalCount); } } 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/quotes_big_amount.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quotes_big_amount.php new file mode 100644 index 0000000000000..c666f938d9125 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quotes_big_amount.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteRepository; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var $objectManager ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = $objectManager->get(QuoteFactory::class); +/** @var QuoteRepository $quoteRepository */ +$quoteRepository = $objectManager->get(QuoteRepository::class); +/** @var StoreRepository $storeRepository */ +$storeRepository = $objectManager->get(StoreRepository::class); +/** @var Config $appConfig */ +$appConfig = $objectManager->get(Config::class); +$appConfig->clean(); + +/** @var Store $defaultStore */ +$defaultStore = $storeRepository->getActiveStoreByCode('default'); + +for ($i = 0; $i < 1000; $i++) { + /** @var Quote $quote */ + $quote = $quoteFactory->create(); + $quote->setStoreId($defaultStore->getId()); + $quoteRepository->save($quote); +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quotes_big_amount_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quotes_big_amount_rollback.php new file mode 100644 index 0000000000000..24832a297949d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quotes_big_amount_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Registry; +use Magento\Quote\Model\QuoteRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var QuoteRepository $quoteRepository */ +$quoteRepository = $objectManager->get(QuoteRepository::class); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->create(); +$items = $quoteRepository->getList($searchCriteria) + ->getItems(); +foreach ($items as $item) { + $quoteRepository->delete($item); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/shipment_for_two_items.php b/dev/tests/integration/testsuite/Magento/Sales/_files/shipment_for_two_items.php new file mode 100644 index 0000000000000..e77134b1e5d4b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/shipment_for_two_items.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\ShipOrderInterface; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/customer_order_with_two_items.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000555'); +/** @var ShipOrderInterface $invoiceOrder */ +$shipOrder = $objectManager->get(ShipOrderInterface::class); + +$shipOrder->execute($order->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/shipment_for_two_items_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/shipment_for_two_items_rollback.php new file mode 100644 index 0000000000000..438b097dd141b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/shipment_for_two_items_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/customer_order_with_two_items_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/Model/Rule/Action/Discount/CartFixedTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php index 5fc6fac193ff6..7ddb38ac94e5d 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php @@ -9,9 +9,16 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ProductRepository; use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; use Magento\Multishipping\Model\Checkout\Type\Multishipping; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartItemInterface; @@ -27,7 +34,10 @@ use Magento\Sales\Model\Order\Email\Sender\OrderSender; use Magento\SalesRule\Api\RuleRepositoryInterface; use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\RuleFactory; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * Tests for Magento\SalesRule\Model\Rule\Action\Discount\CartFixed. @@ -35,7 +45,7 @@ * @magentoAppArea frontend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CartFixedTest extends \PHPUnit\Framework\TestCase +class CartFixedTest extends TestCase { /** * @var GuestCartManagementInterface @@ -53,7 +63,7 @@ class CartFixedTest extends \PHPUnit\Framework\TestCase private $couponManagement; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; @@ -86,6 +96,9 @@ protected function setUp(): void * * @param array $productPrices * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException * @magentoDbIsolation enabled * @magentoAppIsolation enabled * @magentoDataFixture Magento/SalesRule/_files/coupon_cart_fixed_discount.php @@ -139,7 +152,7 @@ public function testOrderWithFixedDiscount(): void $this->quoteRepository->save($quote); $this->assertEquals($expectedGrandTotal, $quote->getGrandTotal()); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + /** @var QuoteIdMask $quoteIdMask */ $quoteIdMask = $this->objectManager->create(QuoteIdMask::class); $quoteIdMask->load($quote->getId(), 'quote_id'); Bootstrap::getInstance()->reinitialize(); @@ -269,8 +282,8 @@ private function createProduct(float $price): ProductInterface ->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) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) ->setStockData(['qty' => 1, 'is_in_stock' => 1]) ->setWeight(1); @@ -310,6 +323,7 @@ private function getOrder(string $incrementId): OrderInterface * @param array $secondOrderTotals * @param array $thirdOrderTotals * @return void + * @throws LocalizedException */ public function testMultishipping( float $discount, @@ -317,7 +331,7 @@ public function testMultishipping( array $secondOrderTotals, array $thirdOrderTotals ): void { - $store = $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore(); + $store = $this->objectManager->get(StoreManagerInterface::class)->getStore(); $salesRule = $this->getRule('15$ fixed discount on whole cart'); $salesRule->setDiscountAmount($discount); $this->saveRule($salesRule); @@ -417,106 +431,106 @@ public function multishippingDataProvider(): array 5, [ 'subtotal' => 10.00, - 'discount_amount' => -5.00, + 'discount_amount' => -1.4300, 'shipping_amount' => 5.00, - 'grand_total' => 10.00, + 'grand_total' => 13.5700, ], [ 'subtotal' => 20.00, - 'discount_amount' => -0.00, + 'discount_amount' => -2.8600, 'shipping_amount' => 5.00, - 'grand_total' => 25.00, + 'grand_total' => 22.1400, ], [ 'subtotal' => 5.00, - 'discount_amount' => -0.00, + 'discount_amount' => -5.00, 'shipping_amount' => 0.00, - 'grand_total' => 5.00, + 'grand_total' => 0.00, ] ], 'Discount = 1stOrderSubtotal: only 1st order gets discount' => [ 10, [ 'subtotal' => 10.00, - 'discount_amount' => -10.00, + 'discount_amount' => -2.8600, 'shipping_amount' => 5.00, - 'grand_total' => 5.00, + 'grand_total' => 12.1400, ], [ 'subtotal' => 20.00, - 'discount_amount' => -0.00, + 'discount_amount' => -5.71, 'shipping_amount' => 5.00, - 'grand_total' => 25.00, + 'grand_total' => 19.2900, ], [ 'subtotal' => 5.00, - 'discount_amount' => -0.00, + 'discount_amount' => -5.00, 'shipping_amount' => 0.00, - 'grand_total' => 5.00, + 'grand_total' => 0.00, ] ], 'Discount > 1stOrderSubtotal: 1st order get 100% discount and 2nd order get the remaining discount' => [ 15, [ 'subtotal' => 10.00, - 'discount_amount' => -10.00, + 'discount_amount' => -4.2900, 'shipping_amount' => 5.00, - 'grand_total' => 5.00, + 'grand_total' => 10.71, ], [ 'subtotal' => 20.00, - 'discount_amount' => -5.00, + 'discount_amount' => -8.5700, 'shipping_amount' => 5.00, - 'grand_total' => 20.00, + 'grand_total' => 16.43, ], [ 'subtotal' => 5.00, - 'discount_amount' => -0.00, + 'discount_amount' => -5.00, 'shipping_amount' => 0.00, - 'grand_total' => 5.00, + 'grand_total' => 0.00, ] ], 'Discount = 1stOrderSubtotal + 2ndOrderSubtotal: 1st order and 2nd order get 100% discount' => [ 30, [ 'subtotal' => 10.00, - 'discount_amount' => -10.00, + 'discount_amount' => -8.5700, 'shipping_amount' => 5.00, - 'grand_total' => 5.00, + 'grand_total' => 6.4300, ], [ 'subtotal' => 20.00, - 'discount_amount' => -20.00, + 'discount_amount' => -17.1400, 'shipping_amount' => 5.00, - 'grand_total' => 5.00, + 'grand_total' => 7.8600, ], [ 'subtotal' => 5.00, - 'discount_amount' => -0.00, + 'discount_amount' => -5.00, 'shipping_amount' => 0.00, - 'grand_total' => 5.00, + 'grand_total' => 0.00, ] ], - 'Discount > 1stOrdSubtotal + 2ndOrdSubtotal: 1st order and 2nd order get 100% discount - and 3rd order get remaining discount' => [ + 'Discount > 1stOrdSubtotal + 2ndOrdSubtotal: 1st order and 2nd order get 100% discount' + . ' and 3rd order get remaining discount' => [ 31, [ 'subtotal' => 10.00, - 'discount_amount' => -10.00, + 'discount_amount' => -8.8600, 'shipping_amount' => 5.00, - 'grand_total' => 5.00, + 'grand_total' => 6.14, ], [ 'subtotal' => 20.00, - 'discount_amount' => -20.00, + 'discount_amount' => -17.7100, 'shipping_amount' => 5.00, - 'grand_total' => 5.00, + 'grand_total' => 7.29, ], [ 'subtotal' => 5.00, - 'discount_amount' => -1.00, + 'discount_amount' => -5.00, 'shipping_amount' => 0.00, - 'grand_total' => 4.00, + 'grand_total' => 0.00, ] ] ]; @@ -545,6 +559,7 @@ private function getOrderList(int $quoteId): array * * @param string $name * @return Rule + * @throws LocalizedException */ private function getRule(string $name): Rule { @@ -559,7 +574,7 @@ private function getRule(string $name): Rule /** @var Rule $salesRule */ $dataModel = array_pop($items); /** @var Rule $ruleModel */ - $ruleModel = $this->objectManager->get(\Magento\SalesRule\Model\RuleFactory::class)->create(); + $ruleModel = $this->objectManager->get(RuleFactory::class)->create(); $ruleModel->load($dataModel->getRuleId()); return $ruleModel; } 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/Security/Model/Plugin/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AccountManagementTest.php new file mode 100644 index 0000000000000..9f878f6f51370 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/Model/Plugin/AccountManagementTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Security\Model\Plugin; + +use Laminas\Stdlib\Parameters; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Model\AccountManagement as CustomerAccountManagement; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\SecurityViolationException; +use Magento\Framework\Module\Manager; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Phrase; +use Magento\Security\Model\ConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use PHPUnit\Framework\TestCase; + +/** + * Tests for account manager plugin. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AccountManagementTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Manager */ + private $moduleManager; + + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var ConfigInterface */ + private $securityConfig; + + /** @var RequestInterface */ + private $request; + + /** @var Phrase */ + private $errorMessage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->moduleManager = $this->objectManager->get(Manager::class); + //This check is needed because Magento_Security independent of Magento_Customer + if (!$this->moduleManager->isEnabled('Magento_Customer')) { + $this->markTestSkipped('Magento_Customer module disabled.'); + } + $this->accountManagement = $this->objectManager->get(AccountManagementInterface::class); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->securityConfig = $this->objectManager->get(ConfigInterface::class); + $this->errorMessage = __( + 'We received too many requests for password resets. Please wait and try again later or contact %1.', + $this->securityConfig->getCustomerServiceEmail() + ); + } + + /** + * @return void + */ + public function testPluginIsRegistered(): void + { + $pluginInfo = $this->objectManager->get(PluginList::class)->get(CustomerAccountManagement::class); + $this->assertSame( + AccountManagement::class, + $pluginInfo['security_check_customer_password_reset_attempt']['instance'] + ); + } + + /** + * @magentoConfigFixture current_store customer/password/max_number_password_reset_requests 1 + * @magentoDataFixture Magento/Security/_files/customer_reset_password.php + * + * @return void + */ + public function testMaxNumberPasswordResetRequests(): void + { + $this->prepareServerParameters(); + $this->expectExceptionObject(new SecurityViolationException($this->errorMessage)); + $this->accountManagement->initiatePasswordReset( + 'customer@example.com', + CustomerAccountManagement::EMAIL_REMINDER + ); + } + + /** + * @magentoConfigFixture current_store customer/password/min_time_between_password_reset_requests 10 + * @magentoDataFixture Magento/Security/_files/customer_reset_password.php + * + * @return void + */ + public function testTimeBetweenPasswordResetRequests(): void + { + $this->prepareServerParameters(); + $this->expectExceptionObject(new SecurityViolationException($this->errorMessage)); + $this->accountManagement->initiatePasswordReset( + 'customer@example.com', + CustomerAccountManagement::EMAIL_REMINDER + ); + } + + /** + * @magentoConfigFixture current_store customer/password/password_reset_protection_type 0 + * @magentoConfigFixture current_store customer/password/max_number_password_reset_requests 1 + * @magentoDataFixture Magento/Security/_files/customer_reset_password.php + * + * @return void + */ + public function testPasswordResetProtectionTypeDisabled(): void + { + $this->prepareServerParameters(); + $result = $this->accountManagement->initiatePasswordReset( + 'customer@example.com', + CustomerAccountManagement::EMAIL_REMINDER + ); + $this->assertTrue($result); + } + + /** + * @magentoConfigFixture current_store customer/password/password_reset_protection_type 1 + * @magentoConfigFixture current_store customer/password/max_number_password_reset_requests 1 + * @magentoDataFixture Magento/Security/_files/customer_reset_password.php + * + * @return void + */ + public function testPasswordResetProtectionTypeByIpAndEmail(): void + { + $this->prepareServerParameters(); + $this->expectExceptionObject(new SecurityViolationException($this->errorMessage)); + $this->accountManagement->initiatePasswordReset( + 'customer@example.com', + CustomerAccountManagement::EMAIL_REMINDER + ); + } + + /** + * @magentoConfigFixture current_store customer/password/password_reset_protection_type 2 + * @magentoConfigFixture current_store customer/password/max_number_password_reset_requests 1 + * @magentoDataFixture Magento/Security/_files/customer_reset_password.php + * + * @return void + */ + public function testPasswordResetProtectionTypeByIp(): void + { + $this->markTestSkipped('Test blocked by issue MC-32988.'); + $this->prepareServerParameters(); + $this->expectExceptionObject(new SecurityViolationException($this->errorMessage)); + $this->accountManagement->initiatePasswordReset( + 'customer@example.com', + CustomerAccountManagement::EMAIL_REMINDER + ); + } + + /** + * @magentoConfigFixture current_store customer/password/password_reset_protection_type 3 + * @magentoConfigFixture current_store customer/password/max_number_password_reset_requests 1 + * @magentoDataFixture Magento/Security/_files/customer_reset_password.php + * + * @return void + */ + public function testPasswordResetProtectionTypeByEmail(): void + { + $this->prepareServerParameters(); + $this->expectExceptionObject(new SecurityViolationException($this->errorMessage)); + $this->accountManagement->initiatePasswordReset( + 'customer@example.com', + CustomerAccountManagement::EMAIL_REMINDER + ); + } + + /** + * Prepare server parameters. + * + * @return void + */ + private function prepareServerParameters(): void + { + $parameters = $this->objectManager->create(Parameters::class); + $parameters->set('REMOTE_ADDR', '127.0.0.1'); + $this->request->setServer($parameters); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Security/_files/customer_reset_password.php b/dev/tests/integration/testsuite/Magento/Security/_files/customer_reset_password.php new file mode 100644 index 0000000000000..3ea95460ed2d0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/_files/customer_reset_password.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Module\Manager; +use Magento\Framework\Stdlib\DateTime; +use Magento\Security\Model\PasswordResetRequestEvent; +use Magento\Security\Model\PasswordResetRequestEventFactory; +use Magento\Security\Model\ResourceModel\PasswordResetRequestEvent as PasswordResetRequestEventResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Manager $moduleManager */ +$moduleManager = $objectManager->get(Manager::class); +//This check is needed because Magento_Security independent of Magento_Customer +if ($moduleManager->isEnabled('Magento_Customer')) { + Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + + /** @var PasswordResetRequestEventFactory $passwordResetRequestEventFactory */ + $passwordResetRequestEventFactory = $objectManager->get(PasswordResetRequestEventFactory::class); + /** @var PasswordResetRequestEventResource $passwordResetRequestEventResource */ + $passwordResetRequestEventResource = $objectManager->get(PasswordResetRequestEventResource::class); + + $dateTime = new DateTimeImmutable(); + $passwordResetRequestEvent = $passwordResetRequestEventFactory->create(); + $passwordResetRequestEvent->setRequestType(PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST) + ->setAccountReference('customer@example.com') + ->setIp(ip2long('127.0.0.1')) + ->setCreatedAt($dateTime->modify('-5 minutes')->format(DateTime::DATETIME_PHP_FORMAT))->save(); + $passwordResetRequestEventResource->save($passwordResetRequestEvent); +} diff --git a/dev/tests/integration/testsuite/Magento/Security/_files/customer_reset_password_rollback.php b/dev/tests/integration/testsuite/Magento/Security/_files/customer_reset_password_rollback.php new file mode 100644 index 0000000000000..d2a00c5eebd14 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/_files/customer_reset_password_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Module\Manager; +use Magento\Framework\Stdlib\DateTime; +use Magento\Security\Model\ResourceModel\PasswordResetRequestEvent; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Manager $moduleManager */ +$moduleManager = $objectManager->get(Manager::class); +//This check is needed because Magento_Security independent of Magento_Customer +if ($moduleManager->isEnabled('Magento_Customer')) { + Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); + + /** @var PasswordResetRequestEvent $passwordResetRequestEventResource */ + $passwordResetRequestEventResource = $objectManager->get(PasswordResetRequestEvent::class); + $dateTime = new DateTimeImmutable(); + $passwordResetRequestEventResource->deleteRecordsOlderThen($dateTime->format(DateTime::DATETIME_PHP_FORMAT)); +} 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/MagentoDataFixtureBeforeTransaction/RemoveFixtureTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixtureBeforeTransaction/RemoveFixtureTest.php index d9c97f5ab7a88..c8d83f4d4e382 100644 --- a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixtureBeforeTransaction/RemoveFixtureTest.php +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixtureBeforeTransaction/RemoveFixtureTest.php @@ -39,6 +39,6 @@ protected function setUp(): void */ public function testRemoveFixture(): void { - $this->assertFalse($this->fixtureCallStorage->getFixturePosition('fixture1_first_module.php')); + $this->assertNull($this->fixtureCallStorage->getFixturePosition('fixture1_first_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/integration/testsuite/Magento/Wishlist/Model/WishlistTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/WishlistTest.php index 84ee7d8984cc4..cab007aa6af9c 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Model/WishlistTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/WishlistTest.php @@ -217,6 +217,27 @@ public function testUpdateItemQtyInWishList(): void $this->assertEquals(55, $updatedItem->getQty()); } + /** + * Update description of wishlist item + * + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * + * @return void + */ + public function testUpdateItemDescriptionInWishList(): void + { + $itemDescription = 'Test Description'; + $wishlist = $this->getWishlistByCustomerId->execute(1); + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'simple'); + $item->setDescription($itemDescription); + $this->assertNotNull($item); + $buyRequest = $this->dataObjectFactory->create(['data' => ['qty' => 55]]); + $wishlist->updateItem($item, $buyRequest); + $updatedItem = $this->getWishlistByCustomerId->getItemBySku(1, 'simple'); + $this->assertEquals(55, $updatedItem->getQty()); + $this->assertEquals($itemDescription, $updatedItem->getDescription()); + } + /** * @return void */ diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Plugin/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Plugin/Model/ResourceModel/ProductTest.php new file mode 100644 index 0000000000000..ecf932ce13b74 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Plugin/Model/ResourceModel/ProductTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Plugin\Model\ResourceModel; + +use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use Magento\TestFramework\Wishlist\Model\GetWishlistByCustomerId; +use PHPUnit\Framework\TestCase; + +/** + * Tests for remove quote items plugin. + * + * @see \Magento\Wishlist\Plugin\Model\ResourceModel\Product + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class ProductTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var ProductResourceModel */ + private $productResoure; + + /** @var GetWishlistByCustomerId */ + private $getWishlistByCustomerId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->productResoure = $this->objectManager->get(ProductResourceModel::class); + $this->getWishlistByCustomerId = $this->objectManager->get(GetWishlistByCustomerId::class); + } + + /** + * @return void + */ + public function testPluginIsRegistered(): void + { + $pluginInfo = $this->objectManager->get(PluginList::class)->get(ProductResourceModel::class); + $this->assertSame( + Product::class, + $pluginInfo['cleanups_wishlist_item_after_product_delete']['instance'] + ); + } + + /** + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * + * @return void + */ + public function testDeleteProduct(): void + { + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'simple'); + $this->assertNotNull($item); + $this->productResoure->delete($item->getProduct()); + $this->assertNull($this->getWishlistByCustomerId->getItemBySku(1, 'simple')); + } +} 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/js/jasmine/tests/lib/mage/requirejs/mixins.test.js b/dev/tests/js/jasmine/tests/lib/mage/requirejs/mixins.test.js index 52374a24e8c68..c54473d4d757f 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/requirejs/mixins.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/requirejs/mixins.test.js @@ -183,5 +183,31 @@ define(['rjsResolver', 'mixins'], function (resolver, mixins) { require([name], function () {}); }); + + it('applies mixins for modules that have no dependencies', function (done) { + var name = 'tests/assets/mixins/mixins-applied-no-dependencies', + mixinName = 'tests/assets/mixins/mixins-applied-no-dependencies-ext'; + + mixins.hasMixins.and.returnValue(true); + mixins.getMixins.and.returnValue([mixinName]); + + define(name, { + value: 'original' + }); + + define(mixinName, [], function () { + return function (module) { + module.value = 'changed'; + + return module; + }; + }); + + require([name], function (module) { + expect(module.value).toBe('changed'); + + done(); + }); + }); }); }); 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/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt index 1bb13d553c754..a9c65eb29c2e5 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt @@ -3,9 +3,10 @@ # Example: # app/code/Magento/Catalog # dev/tests/static/framework/bootstrap.php -lib/internal/Magento/Framework/Interception/Test/Unit/Config/ConfigTest.php lib/internal/Magento/Framework/Cache/Backend/Eaccelerator.php lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/InterceptorTest.php +lib/internal/Magento/Framework/Interception/Test/Unit/Config/ConfigTest.php dev/tests/integration/framework/deployTestModules.php dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php dev/tests/integration/testsuite/Magento/Framework/Session/ConfigTest.php 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/Exception/InvalidArgumentException.php b/lib/internal/Magento/Framework/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..a92308ec41bde --- /dev/null +++ b/lib/internal/Magento/Framework/Exception/InvalidArgumentException.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Exception; + +/** + * An exception to be thrown when arguments are invalidated + */ +class InvalidArgumentException extends LocalizedException +{ +} diff --git a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php index 69a0d3029e18d..4a3ed34810723 100644 --- a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php +++ b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php @@ -7,17 +7,11 @@ namespace Magento\Framework\Interception\Code\Generator; -/** - * Class Interceptor - ˚* - * @package Magento\Framework\Interception\Code\Generator - */ -class Interceptor extends \Magento\Framework\Code\Generator\EntityAbstract +use Magento\Framework\Code\Generator\EntityAbstract; + +class Interceptor extends EntityAbstract { - /** - * Entity type - */ - const ENTITY_TYPE = 'interceptor'; + public const ENTITY_TYPE = 'interceptor'; /** * Returns default result class name @@ -52,9 +46,8 @@ protected function _getDefaultConstructorDefinition() $parameters = []; $body = "\$this->___init();\n"; if ($constructor) { - foreach ($constructor->getParameters() as $parameter) { - $parameters[] = $this->_getMethodParameterInfo($parameter); - } + $parameters = array_map([$this, '_getMethodParameterInfo'], $constructor->getParameters()); + $body .= count($parameters) ? "parent::__construct({$this->_getParameterList($parameters)});" : "parent::__construct();"; @@ -70,7 +63,7 @@ protected function _getDefaultConstructorDefinition() /** * Returns list of methods for class generator * - * @return mixed + * @return array */ protected function _getClassMethods() { @@ -107,10 +100,7 @@ protected function isInterceptedMethod(\ReflectionMethod $method) */ protected function _getMethodInfo(\ReflectionMethod $method) { - $parameters = []; - foreach ($method->getParameters() as $parameter) { - $parameters[] = $this->_getMethodParameterInfo($parameter); - } + $parameters = array_map([$this, '_getMethodParameterInfo'], $method->getParameters()); $returnTypeValue = $this->getReturnTypeValue($method); $methodInfo = [ @@ -118,22 +108,18 @@ protected function _getMethodInfo(\ReflectionMethod $method) 'parameters' => $parameters, 'body' => str_replace( [ - '%methodName%', + '%method%', '%return%', '%parameters%' ], [ $method->getName(), - $returnTypeValue === 'void' ? '' : ' return', + $returnTypeValue === 'void' ? '' : 'return ', $this->_getParameterList($parameters) ], <<<'METHOD_BODY' -$pluginInfo = $this->pluginList->getNext($this->subjectType, '%methodName%'); -if (!$pluginInfo) { - %return% parent::%methodName%(%parameters%); -} else { - %return% $this->___callPlugins('%methodName%', func_get_args(), $pluginInfo); -} +$pluginInfo = $this->pluginList->getNext($this->subjectType, '%method%'); +%return%$pluginInfo ? $this->___callPlugins('%method%', func_get_args(), $pluginInfo) : parent::%method%(%parameters%); METHOD_BODY ), 'returnType' => $returnTypeValue, @@ -206,11 +192,7 @@ protected function _validateData() if ($resultClassName !== $sourceClassName . '\\Interceptor') { $this->_addError( - 'Invalid Interceptor class name [' . - $resultClassName . - ']. Use ' . - $sourceClassName . - '\\Interceptor' + 'Invalid Interceptor class name ' . $resultClassName . '. Use ' . $sourceClassName . '\\Interceptor' ); $result = false; } diff --git a/lib/internal/Magento/Framework/Interception/Code/InterfaceValidator.php b/lib/internal/Magento/Framework/Interception/Code/InterfaceValidator.php index 4609fe09f1f0d..b67af21878c3d 100644 --- a/lib/internal/Magento/Framework/Interception/Code/InterfaceValidator.php +++ b/lib/internal/Magento/Framework/Interception/Code/InterfaceValidator.php @@ -5,30 +5,32 @@ */ namespace Magento\Framework\Interception\Code; +use Magento\Framework\Code\Reader\ArgumentsReader; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Phrase; +/** + * @SuppressWarnings(PHPMD.NPathComplexity) + */ class InterfaceValidator { - const METHOD_BEFORE = 'before'; - - const METHOD_AROUND = 'around'; - - const METHOD_AFTER = 'after'; + public const METHOD_BEFORE = 'before'; + public const METHOD_AROUND = 'around'; + public const METHOD_AFTER = 'after'; /** * Arguments reader model * - * @var \Magento\Framework\Code\Reader\ArgumentsReader + * @var ArgumentsReader */ protected $_argumentsReader; /** - * @param \Magento\Framework\Code\Reader\ArgumentsReader $argumentsReader + * @param ArgumentsReader $argumentsReader */ - public function __construct(\Magento\Framework\Code\Reader\ArgumentsReader $argumentsReader = null) + public function __construct(ArgumentsReader $argumentsReader = null) { - $this->_argumentsReader = $argumentsReader ?: new \Magento\Framework\Code\Reader\ArgumentsReader(); + $this->_argumentsReader = $argumentsReader ?? new ArgumentsReader(); } /** @@ -50,7 +52,7 @@ public function validate($pluginClass, $interceptedType) $type = new \ReflectionClass($interceptedType); foreach ($plugin->getMethods(\ReflectionMethod::IS_PUBLIC) as $pluginMethod) { - /** @var $pluginMethod \ReflectionMethod */ + /** @var \ReflectionMethod $pluginMethod */ $originMethodName = $this->getOriginMethodName($pluginMethod->getName()); if ($originMethodName === null) { continue; @@ -63,19 +65,11 @@ public function validate($pluginClass, $interceptedType) ) ); } - $originMethod = $type->getMethod($originMethodName); $pluginMethodParameters = $this->getMethodParameters($pluginMethod); - $originMethodParameters = $this->getMethodParameters($originMethod); - - $methodType = $this->getMethodType($pluginMethod->getName()); - $subject = array_shift($pluginMethodParameters); - if (!$this->_argumentsReader->isCompatibleType( - $subject['type'], - $interceptedType - ) || $subject['type'] === null - ) { + if ($subject['type'] === null + || !$this->_argumentsReader->isCompatibleType($subject['type'], $interceptedType)) { throw new ValidatorException( new Phrase( 'Invalid [%1] $%2 type in %3::%4. It must be compatible with %5', @@ -84,45 +78,50 @@ public function validate($pluginClass, $interceptedType) ); } - switch ($methodType) { - case self::METHOD_BEFORE: - $this->validateMethodsParameters( - $pluginMethodParameters, - $originMethodParameters, - $pluginClass, - $pluginMethod->getName() - ); - break; - case self::METHOD_AROUND: - $proceed = array_shift($pluginMethodParameters); - if (!$this->_argumentsReader->isCompatibleType($proceed['type'], '\\Closure')) { - throw new ValidatorException( - new Phrase( - 'Invalid [%1] $%2 type in %3::%4. It must be compatible with \\Closure', - [$proceed['type'], $proceed['name'], $pluginClass, $pluginMethod->getName()] - ) - ); - } - $this->validateMethodsParameters( - $pluginMethodParameters, - $originMethodParameters, - $pluginClass, - $pluginMethod->getName() + $originMethod = $type->getMethod($originMethodName); + $originMethodParameters = $this->getMethodParameters($originMethod); + $methodType = $this->getMethodType($pluginMethod->getName()); + + if (self::METHOD_AFTER === $methodType && count($pluginMethodParameters) > 1) { + // remove result + array_shift($pluginMethodParameters); + $matchedParameters = array_intersect_key($originMethodParameters, $pluginMethodParameters); + $this->validateMethodsParameters( + $pluginMethodParameters, + $matchedParameters, + $pluginClass, + $pluginMethod->getName() + ); + continue; + } + + if (self::METHOD_BEFORE === $methodType) { + $this->validateMethodsParameters( + $pluginMethodParameters, + $originMethodParameters, + $pluginClass, + $pluginMethod->getName() + ); + continue; + } + + if (self::METHOD_AROUND === $methodType) { + $proceed = array_shift($pluginMethodParameters); + if (!$this->_argumentsReader->isCompatibleType($proceed['type'], '\\Closure')) { + throw new ValidatorException( + new Phrase( + 'Invalid [%1] $%2 type in %3::%4. It must be compatible with \\Closure', + [$proceed['type'], $proceed['name'], $pluginClass, $pluginMethod->getName()] + ) ); - break; - case self::METHOD_AFTER: - if (count($pluginMethodParameters) > 1) { - // remove result - array_shift($pluginMethodParameters); - $matchedParameters = array_intersect_key($originMethodParameters, $pluginMethodParameters); - $this->validateMethodsParameters( - $pluginMethodParameters, - $matchedParameters, - $pluginClass, - $pluginMethod->getName() - ); - } - break; + } + $this->validateMethodsParameters( + $pluginMethodParameters, + $originMethodParameters, + $pluginClass, + $pluginMethod->getName() + ); + continue; } } } @@ -170,8 +169,7 @@ protected function validateMethodsParameters(array $pluginParameters, array $ori protected function getParametersType(\ReflectionParameter $parameter) { $parameterClass = $parameter->getClass(); - $type = $parameterClass ? '\\' . $parameterClass->getName() : ($parameter->isArray() ? 'array' : null); - return $type; + return $parameterClass ? '\\' . $parameterClass->getName() : ($parameter->isArray() ? 'array' : null); } /** @@ -183,17 +181,16 @@ protected function getParametersType(\ReflectionParameter $parameter) */ protected function getOriginMethodName($pluginMethodName) { - switch ($this->getMethodType($pluginMethodName)) { - case self::METHOD_BEFORE: - case self::METHOD_AROUND: - return lcfirst(substr($pluginMethodName, 6)); + $methodType = $this->getMethodType($pluginMethodName); - case self::METHOD_AFTER: - return lcfirst(substr($pluginMethodName, 5)); - - default: - return null; + if (self::METHOD_AFTER === $methodType) { + return lcfirst(substr($pluginMethodName, 5)); + } + if (self::METHOD_BEFORE === $methodType || self::METHOD_AROUND === $methodType) { + return lcfirst(substr($pluginMethodName, 6)); } + + return null; } /** @@ -205,12 +202,14 @@ protected function getOriginMethodName($pluginMethodName) */ protected function getMethodType($pluginMethodName) { - if (substr($pluginMethodName, 0, 6) == self::METHOD_BEFORE) { + if (0 === strpos($pluginMethodName, self::METHOD_AFTER)) { + return self::METHOD_AFTER; + } + if (0 === strpos($pluginMethodName, self::METHOD_BEFORE)) { return self::METHOD_BEFORE; - } elseif (substr($pluginMethodName, 0, 6) == self::METHOD_AROUND) { + } + if (0 === strpos($pluginMethodName, self::METHOD_AROUND)) { return self::METHOD_AROUND; - } elseif (substr($pluginMethodName, 0, 5) == self::METHOD_AFTER) { - return self::METHOD_AFTER; } return null; 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/PluginList/PluginList.php b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php index bf1372dc007a1..610ae9473a725 100644 --- a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php +++ b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php @@ -281,9 +281,18 @@ protected function _loadScopedData() { $scope = $this->_configScope->getCurrentScope(); if (false == isset($this->_loadedScopes[$scope])) { - if (false == in_array($scope, $this->_scopePriorityScheme)) { - $this->_scopePriorityScheme[] = $scope; + $index = array_search($scope, $this->_scopePriorityScheme); + /** + * Force current scope to be at the end of the scheme to ensure that default priority scopes are loaded. + * Mostly happens when the current scope is primary. + * For instance if the default scope priority scheme is [primary, global] and current scope is primary, + * the resulted scheme will be [global, primary] so global scope is loaded. + */ + if ($index !== false) { + unset($this->_scopePriorityScheme[$index]); } + $this->_scopePriorityScheme[] = $scope; + $cacheId = implode('|', $this->_scopePriorityScheme) . "|" . $this->_cacheId; $data = $this->_cache->load($cacheId); if ($data) { diff --git a/lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/_files/Interceptor.txt b/lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/_files/Interceptor.txt index fe9b4dad7022e..87646be998c0a 100644 --- a/lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/_files/Interceptor.txt +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/_files/Interceptor.txt @@ -18,11 +18,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\Sample public function getValue() { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'getValue'); - if (!$pluginInfo) { - return parent::getValue(); - } else { - return $this->___callPlugins('getValue', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('getValue', func_get_args(), $pluginInfo) : parent::getValue(); } /** @@ -31,11 +27,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\Sample public function setValue($value) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'setValue'); - if (!$pluginInfo) { - return parent::setValue($value); - } else { - return $this->___callPlugins('setValue', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('setValue', func_get_args(), $pluginInfo) : parent::setValue($value); } /** @@ -44,11 +36,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\Sample public function & getReference() { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'getReference'); - if (!$pluginInfo) { - return parent::getReference(); - } else { - return $this->___callPlugins('getReference', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('getReference', func_get_args(), $pluginInfo) : parent::getReference(); } /** @@ -57,11 +45,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\Sample public function firstVariadicParameter(... $variadicValue) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'firstVariadicParameter'); - if (!$pluginInfo) { - return parent::firstVariadicParameter(... $variadicValue); - } else { - return $this->___callPlugins('firstVariadicParameter', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('firstVariadicParameter', func_get_args(), $pluginInfo) : parent::firstVariadicParameter(... $variadicValue); } /** @@ -70,11 +54,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\Sample public function secondVariadicParameter($value, ... $variadicValue) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'secondVariadicParameter'); - if (!$pluginInfo) { - return parent::secondVariadicParameter($value, ... $variadicValue); - } else { - return $this->___callPlugins('secondVariadicParameter', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('secondVariadicParameter', func_get_args(), $pluginInfo) : parent::secondVariadicParameter($value, ... $variadicValue); } /** @@ -83,10 +63,6 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\Sample public function byRefVariadic(&... $variadicValue) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'byRefVariadic'); - if (!$pluginInfo) { - return parent::byRefVariadic(... $variadicValue); - } else { - return $this->___callPlugins('byRefVariadic', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('byRefVariadic', func_get_args(), $pluginInfo) : parent::byRefVariadic(... $variadicValue); } } diff --git a/lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/_files/TInterceptor.txt b/lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/_files/TInterceptor.txt index 71ea5b0475cfc..5ff09d204c5ef 100644 --- a/lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/_files/TInterceptor.txt +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/_files/TInterceptor.txt @@ -18,11 +18,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\TSample public function returnVoid() : void { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'returnVoid'); - if (!$pluginInfo) { - parent::returnVoid(); - } else { - $this->___callPlugins('returnVoid', func_get_args(), $pluginInfo); - } + $pluginInfo ? $this->___callPlugins('returnVoid', func_get_args(), $pluginInfo) : parent::returnVoid(); } /** @@ -31,11 +27,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\TSample public function getNullableValue() : ?string { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'getNullableValue'); - if (!$pluginInfo) { - return parent::getNullableValue(); - } else { - return $this->___callPlugins('getNullableValue', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('getNullableValue', func_get_args(), $pluginInfo) : parent::getNullableValue(); } /** @@ -44,11 +36,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\TSample public function getValue() : string { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'getValue'); - if (!$pluginInfo) { - return parent::getValue(); - } else { - return $this->___callPlugins('getValue', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('getValue', func_get_args(), $pluginInfo) : parent::getValue(); } /** @@ -57,11 +45,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\TSample public function setValue(string $value) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'setValue'); - if (!$pluginInfo) { - return parent::setValue($value); - } else { - return $this->___callPlugins('setValue', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('setValue', func_get_args(), $pluginInfo) : parent::setValue($value); } /** @@ -70,11 +54,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\TSample public function typeHintedFirstVariadicParameter(string ... $variadicValue) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'typeHintedFirstVariadicParameter'); - if (!$pluginInfo) { - return parent::typeHintedFirstVariadicParameter(... $variadicValue); - } else { - return $this->___callPlugins('typeHintedFirstVariadicParameter', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('typeHintedFirstVariadicParameter', func_get_args(), $pluginInfo) : parent::typeHintedFirstVariadicParameter(... $variadicValue); } /** @@ -83,11 +63,7 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\TSample public function typeHintedSecondVariadicParameter(string $value, string ... $variadicValue) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'typeHintedSecondVariadicParameter'); - if (!$pluginInfo) { - return parent::typeHintedSecondVariadicParameter($value, ... $variadicValue); - } else { - return $this->___callPlugins('typeHintedSecondVariadicParameter', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('typeHintedSecondVariadicParameter', func_get_args(), $pluginInfo) : parent::typeHintedSecondVariadicParameter($value, ... $variadicValue); } /** @@ -96,10 +72,6 @@ class Interceptor extends \Magento\Framework\Interception\Code\Generator\TSample public function byRefTypeHintedVariadic(string &... $variadicValue) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'byRefTypeHintedVariadic'); - if (!$pluginInfo) { - return parent::byRefTypeHintedVariadic(... $variadicValue); - } else { - return $this->___callPlugins('byRefTypeHintedVariadic', func_get_args(), $pluginInfo); - } + return $pluginInfo ? $this->___callPlugins('byRefTypeHintedVariadic', func_get_args(), $pluginInfo) : parent::byRefTypeHintedVariadic(... $variadicValue); } } 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/Interception/Test/Unit/PluginList/PluginListTest.php b/lib/internal/Magento/Framework/Interception/Test/Unit/PluginList/PluginListTest.php index 0122e0ae0507b..56740268026c2 100644 --- a/lib/internal/Magento/Framework/Interception/Test/Unit/PluginList/PluginListTest.php +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/PluginList/PluginListTest.php @@ -68,7 +68,7 @@ class PluginListTest extends TestCase private $serializerMock; /** - * @var ObjectManagerInterface||\PHPUnit\Framework\MockObject\MockObject + * @var ObjectManagerInterface|MockObject */ private $objectManagerMock; @@ -172,15 +172,23 @@ public function testGetPlugin() } /** - * @param $expectedResult - * @param $type - * @param $method - * @param $scopeCode + * @param array $expectedResult + * @param string $type + * @param string $method + * @param string $scopeCode * @param string $code + * @param array $scopePriorityScheme * @dataProvider getPluginsDataProvider */ - public function testGetPlugins($expectedResult, $type, $method, $scopeCode, $code = '__self') - { + public function testGetPlugins( + ?array $expectedResult, + string $type, + string $method, + string $scopeCode, + string $code = '__self', + array $scopePriorityScheme = ['global'] + ): void { + $this->setScopePriorityScheme($scopePriorityScheme); $this->configScopeMock->expects( $this->any() )->method( @@ -245,7 +253,47 @@ public function getPluginsDataProvider() ItemContainer::class, 'getName', 'backend' - ] + ], + [ + // even though the scope is primary, both primary and global scopes are loaded + // because global is in default priority scheme + [ + 4 => [ + 'primary_plugin', + 'simple_plugin', + ] + ], + \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class, + 'getName', + 'primary', + '__self', + ['primary', 'global'] + ], + [ + [ + 4 => [ + 'primary_plugin', + 'simple_plugin', + ] + ], + \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class, + 'getName', + 'global', + '__self', + ['primary', 'global'] + ], + [ + [ + 4 => [ + 'primary_plugin', + ] + ], + \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class, + 'getName', + 'frontend', + '__self', + ['primary', 'global'] + ], ]; } @@ -348,4 +396,16 @@ public function testLoadScopeDataWithEmptyData() ) ); } + + /** + * @param array $areaCodes + * @throws \ReflectionException + */ + private function setScopePriorityScheme(array $areaCodes): void + { + $reflection = new \ReflectionClass($this->object); + $reflection_property = $reflection->getProperty('_scopePriorityScheme'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->object, $areaCodes); + } } diff --git a/lib/internal/Magento/Framework/Interception/Test/Unit/_files/reader_mock_map.php b/lib/internal/Magento/Framework/Interception/Test/Unit/_files/reader_mock_map.php index 7698210126af7..d542faec95c69 100644 --- a/lib/internal/Magento/Framework/Interception/Test/Unit/_files/reader_mock_map.php +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/_files/reader_mock_map.php @@ -3,19 +3,38 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item\Enhanced; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainer; use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainerPlugin\Simple; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Advanced; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Simple as ItemPluginSimple; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash; use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash\Plugin; return [ + [ + 'primary', + [ + Item::class => [ + 'plugins' => [ + 'primary_plugin' => [ + 'sortOrder' => 1, + 'instance' => ItemPluginSimple::class, + ], + ], + ] + ], + ], [ 'global', [ - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class => [ + Item::class => [ 'plugins' => [ 'simple_plugin' => [ 'sortOrder' => 10, - 'instance' => - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Simple::class, + 'instance' => ItemPluginSimple::class, ], ], ] @@ -24,16 +43,15 @@ [ 'backend', [ - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class => [ + Item::class => [ 'plugins' => [ 'advanced_plugin' => [ 'sortOrder' => 5, - 'instance' => - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Advanced::class, + 'instance' => Advanced::class, ], ], ], - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainer::class => [ + ItemContainer::class => [ 'plugins' => [ 'simple_plugin' => [ 'sortOrder' => 15, @@ -41,7 +59,7 @@ ], ], ], - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash::class => [ + StartingBackslash::class => [ 'plugins' => [ 'simple_plugin' => [ 'sortOrder' => 20, @@ -53,14 +71,19 @@ ], [ 'frontend', - [\Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class => [ - 'plugins' => ['simple_plugin' => ['disabled' => true]], - ], \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item\Enhanced::class => [ + [ + Item::class => [ + 'plugins' => [ + 'simple_plugin' => [ + 'disabled' => true + ] + ], + ], + Enhanced::class => [ 'plugins' => [ 'advanced_plugin' => [ 'sortOrder' => 5, - 'instance' => - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Advanced::class, + 'instance' => Advanced::class, ], ], ], 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/gallery/gallery.js b/lib/web/mage/gallery/gallery.js index 02ba72f64127b..6309fa267ea93 100644 --- a/lib/web/mage/gallery/gallery.js +++ b/lib/web/mage/gallery/gallery.js @@ -270,7 +270,10 @@ define([ next: $t('Next'), previous: $t('Previous') }), - mainImageIndex; + mainImageIndex, + $element = settings.$element, + $fotoramaElement, + $fotoramaStage; if (settings.breakpoints) { _.each(_.values(settings.breakpoints), function (breakpoint) { @@ -285,19 +288,38 @@ define([ settings.breakpoints = breakpoints; } - _.extend(config, config.options); - config.options = undefined; - - config.click = false; - config.breakpoints = null; + _.extend(config, config.options, + { + options: undefined, + click: false, + breakpoints: null + } + ); settings.currentConfig = config; - settings.$element.css('min-height', settings.$element.height()); - settings.$element.html(tpl); - settings.$element.removeClass('_block-content-loading'); - settings.$elementF = $(settings.$element.children()[0]); - settings.$elementF.fotorama(config); - settings.$element.css('min-height', ''); - settings.fotoramaApi = settings.$elementF.data('fotorama'); + + $element + .css('min-height', settings.$element.height()) + .append(tpl); + + $fotoramaElement = $element.find('[data-gallery-role="gallery"]'); + + $fotoramaStage = $fotoramaElement.find('.fotorama__stage'); + $fotoramaStage.css('position', 'absolute'); + + $fotoramaElement.fotorama(config); + $fotoramaElement.find('.fotorama__stage__frame.fotorama__active') + .one('f:load', function () { + // Remove placeholder when main gallery image loads. + $element.find('.gallery-placeholder__image').remove(); + $element + .removeClass('_block-content-loading') + .css('min-height', ''); + + $fotoramaStage.css('position', ''); + }); + settings.$elementF = $fotoramaElement; + settings.fotoramaApi = $fotoramaElement.data('fotorama'); + $.extend(true, config, this.startConfig); mainImageIndex = getMainImageIndex(config.data); diff --git a/lib/web/mage/gallery/gallery.less b/lib/web/mage/gallery/gallery.less index 10fba50fe239b..86ccdb858bf19 100644 --- a/lib/web/mage/gallery/gallery.less +++ b/lib/web/mage/gallery/gallery.less @@ -395,6 +395,11 @@ position: absolute; top: 0; width: @fotorama-arw-size; + + ._block-content-loading & { + opacity: 0; + } + .fotorama__arr__arr { &:extend(.fotorama-sprite); .fotorama-abs-center(); 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/polyfill.js b/lib/web/mage/polyfill.js index 94232b29404fa..478b6544bd379 100644 --- a/lib/web/mage/polyfill.js +++ b/lib/web/mage/polyfill.js @@ -1,19 +1,21 @@ -try { - if (!window.localStorage || !window.sessionStorage) { - throw new Error(); - } +(function (root, doc) { + 'use strict'; + + var Storage; - localStorage.setItem('storage_test', 1); - localStorage.removeItem('storage_test'); -} catch (e) { - (function () { - 'use strict'; + try { + if (!root.localStorage || !root.sessionStorage) { + throw new Error(); + } + localStorage.setItem('storage_test', 1); + localStorage.removeItem('storage_test'); + } catch (e) { /** * Returns a storage object to shim local or sessionStorage * @param {String} type - either 'local' or 'session' */ - var Storage = function (type) { + Storage = function (type) { var data; /** @@ -32,7 +34,7 @@ try { } else { expires = ''; } - document.cookie = name + '=' + value + expires + '; path=/'; + doc.cookie = name + '=' + value + expires + '; path=/'; } /** @@ -41,7 +43,7 @@ try { */ function readCookie(name) { var nameEQ = name + '=', - ca = document.cookie.split(';'), + ca = doc.cookie.split(';'), i = 0, c; @@ -70,11 +72,11 @@ try { return 'localstorage'; } - if (!window.name) { - window.name = new Date().getTime(); + if (!root.name) { + root.name = new Date().getTime(); } - return 'sessionStorage' + window.name; + return 'sessionStorage' + root.name; } /** @@ -170,7 +172,7 @@ try { }; }; - window.localStorage.prototype = window.localStorage = new Storage('local'); - window.sessionStorage.prototype = window.sessionStorage = new Storage('session'); - })(); -} + root.localStorage.prototype = root.localStorage = new Storage('local'); + root.sessionStorage.prototype = root.sessionStorage = new Storage('session'); + } +})(window, document); diff --git a/lib/web/mage/requirejs/mixins.js b/lib/web/mage/requirejs/mixins.js index 613605038f4b9..cb5ee8e462054 100644 --- a/lib/web/mage/requirejs/mixins.js +++ b/lib/web/mage/requirejs/mixins.js @@ -232,9 +232,12 @@ require([ * from it every time require call happens. */ defContext.defQueue.shift = function () { - var queueItem = Array.prototype.shift.call(this); + var queueItem = Array.prototype.shift.call(this), + lastDeps = queueItem && queueItem[1]; - queueItem[1] = processNames(queueItem[1], defContext); + if (Array.isArray(lastDeps)) { + queueItem[1] = processNames(queueItem[1], defContext); + } return queueItem; }; 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 20f9ca6c9c50b..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. @@ -817,6 +818,28 @@ public function declarativeInstallSchema(array $request) $this->getDeclarationInstaller()->installSchema($request); } + /** + * Clear memory tables + * + * Memory tables that used in old versions of Magento for indexing purposes should be cleaned + * Otherwise some supported DB solutions like Galeracluster may have replication error + * when memory engine will be switched to InnoDb + * + * @param SchemaSetupInterface $setup + * @return void + */ + private function cleanMemoryTables(SchemaSetupInterface $setup) + { + $connection = $setup->getConnection(); + $tables = $connection->getTables(); + foreach ($tables as $table) { + $tableData = $connection->showTableStatus($table); + if (isset($tableData['Engine']) && $tableData['Engine'] === 'MEMORY') { + $connection->truncateTable($table); + } + } + } + /** * Installs DB schema * @@ -835,6 +858,7 @@ public function installSchema(array $request) $setup = $this->setupFactory->create($this->context->getResources()); $this->setupModuleRegistry($setup); $this->setupCoreTables($setup); + $this->cleanMemoryTables($setup); $this->log->log('Schema creation/updates:'); $this->declarativeInstallSchema($request); $this->handleDBSchemaData($setup, 'schema', $request); @@ -1288,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); @@ -1308,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)); + } } /** @@ -1321,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 6b9e2c8bb549c..8446486c2f104 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php @@ -326,9 +326,10 @@ public function testInstall(array $request, array $logMessages) $setup = $this->createMock(Setup::class); $table = $this->createMock(Table::class); $connection = $this->getMockBuilder(AdapterInterface::class) - ->setMethods(['getSchemaListener', 'newTable']) + ->setMethods(['getSchemaListener', 'newTable', 'getTables']) ->getMockForAbstractClass(); $connection->expects($this->any())->method('getSchemaListener')->willReturn($this->schemaListenerMock); + $connection->expects($this->once())->method('getTables')->willReturn([]); $setup->expects($this->any())->method('getConnection')->willReturn($connection); $table->expects($this->any())->method('addColumn')->willReturn($table); $table->expects($this->any())->method('setComment')->willReturn($table); @@ -449,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:'], @@ -462,7 +463,6 @@ public function installDataProvider() ['Module \'Bar_Two\':'], ['Enabling caches:'], ['Current status:'], - [print_r([], true)], ['Caches clearing:'], ['Cache cleared successfully'], ['Disabling Maintenance Mode:'], @@ -501,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:'], @@ -514,7 +514,6 @@ public function installDataProvider() ['Module \'Bar_Two\':'], ['Enabling caches:'], ['Current status:'], - [print_r([], true)], ['Installing admin user...'], ['Caches clearing:'], ['Cache cleared successfully'],