diff --git a/app/code/Magento/Bundle/Model/Product/LinksList.php b/app/code/Magento/Bundle/Model/Product/LinksList.php index aeb71d0e434ab..c35d475e04d84 100644 --- a/app/code/Magento/Bundle/Model/Product/LinksList.php +++ b/app/code/Magento/Bundle/Model/Product/LinksList.php @@ -39,6 +39,8 @@ public function __construct( } /** + * Bundle Product Items Data + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param int $optionId * @return \Magento\Bundle\Api\Data\LinkInterface[] @@ -50,8 +52,12 @@ public function getItems(\Magento\Catalog\Api\Data\ProductInterface $product, $o $productLinks = []; /** @var \Magento\Catalog\Model\Product $selection */ foreach ($selectionCollection as $selection) { + $bundledProductPrice = $selection->getSelectionPriceValue(); + if ($bundledProductPrice <= 0) { + $bundledProductPrice = $selection->getPrice(); + } $selectionPriceType = $product->getPriceType() ? $selection->getSelectionPriceType() : null; - $selectionPrice = $product->getPriceType() ? $selection->getSelectionPriceValue() : null; + $selectionPrice = $bundledProductPrice ? $bundledProductPrice : null; /** @var \Magento\Bundle\Api\Data\LinkInterface $productLink */ $productLink = $this->linkFactory->create(); diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml new file mode 100644 index 0000000000000..0e2ae9bf5cc5f --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml @@ -0,0 +1,94 @@ + + + + + + + + + <description value="Customer should be able to see all the bundle items in invoice view"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37515"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct2" stepKey="firstSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProduct"/> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="firstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="secondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Create new bundle product --> + <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createBundleProduct"> + <argument name="productType" value="bundle"/> + </actionGroup> + + <!-- Fill all main fields --> + <actionGroup ref="FillMainBundleProductFormActionGroup" stepKey="fillMainProductFields"/> + + <!-- Add first bundle option to the product --> + <actionGroup ref="AddBundleOptionWithTwoProductsActionGroup" stepKey="addFirstBundleOption"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$firstSimpleProduct.sku$"/> + <argument name="prodTwoSku" value="$secondSimpleProduct.sku$$"/> + <argument name="optionTitle" value="{{CheckboxOption.title}}"/> + <argument name="inputType" value="{{CheckboxOption.type}}"/> + </actionGroup> + + <!-- Save product form --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveWithThreeOptions"/> + + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!--Open Product Page--> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> + <argument name="productUrl" value="{{BundleProduct.name}}"/> + </actionGroup> + + <!-- Add bundle to cart --> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"> + <argument name="productUrl" value="{{BundleProduct.name}}"/> + </actionGroup> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts(CheckboxOption.title, '1')}}" stepKey="selectOption2Product1"/> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts(CheckboxOption.title, '2')}}" stepKey="selectOption2Product2"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + + <!--Navigate to checkout--> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/> + <!-- Click next button to open payment section --> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!-- Click place order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Order review page has address that was created during checkout --> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrdersGridById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + + <!-- Open create invoice page --> + <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> + + <!-- Assert item options display --> + <see selector="{{AdminInvoiceItemsSection.bundleItem}}" userInput="50 x $firstSimpleProduct.sku$" stepKey="seeFirstProductInList"/> + <see selector="{{AdminInvoiceItemsSection.bundleItem}}" userInput="50 x $secondSimpleProduct.sku$" stepKey="seeSecondProductInList"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php index fbc3b5e87ac97..27531682b1de2 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php @@ -91,7 +91,7 @@ public function testLinksList() ->method('getSelectionsCollection') ->with([$optionId], $this->productMock) ->willReturn([$this->selectionMock]); - $this->productMock->expects($this->exactly(2))->method('getPriceType')->willReturn('price_type'); + $this->productMock->expects($this->once())->method('getPriceType')->willReturn('price_type'); $this->selectionMock->expects($this->once()) ->method('getSelectionPriceType') ->willReturn('selection_price_type'); diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml index 23e7ef27fa78d..15ef3c311e396 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml @@ -15,6 +15,7 @@ <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php $canEditItemQty = true ?> <?php /** @var \Magento\Catalog\Helper\Data $catalogHelper */ $catalogHelper = $block->getData('catalogHelper'); @@ -37,7 +38,7 @@ $catalogHelper = $block->getData('catalogHelper'); <?php if ($_item->getOrderItem()->getParentItem()): ?> <?php if ($shipTogether) { - continue; + $canEditItemQty = false; } ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> @@ -130,7 +131,7 @@ $catalogHelper = $block->getData('catalogHelper'); </td> <td class="col-qty-invoice"> <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> - <?php if ($block->canEditQty()): ?> + <?php if ($block->canEditQty() && $canEditItemQty): ?> <input type="text" class="input-text admin__control-text qty-input" name="invoice[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php index a25501d9ef150..6cc5652352154 100644 --- a/app/code/Magento/Catalog/Block/Product/View.php +++ b/app/code/Magento/Catalog/Block/Product/View.php @@ -196,6 +196,10 @@ public function getJsonConfig() 'productId' => (int)$product->getId(), 'priceFormat' => $this->_localeFormat->getPriceFormat(), 'prices' => [ + 'baseOldPrice' => [ + 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() * 1, + 'adjustments' => [] + ], 'oldPrice' => [ 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getValue() * 1, 'adjustments' => [] diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index a06266037d05c..ab74b5694ce9f 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -384,7 +384,9 @@ public function backgroundColor($colorRGB) { // assume that 3 params were given instead of array if (!is_array($colorRGB)) { + //phpcs:disable $colorRGB = func_get_args(); + //phpcs:enabled } $this->_getModel()->setBackgroundColor($colorRGB); return $this; @@ -498,7 +500,11 @@ protected function initBaseFile() if ($this->getImageFile()) { $model->setBaseFile($this->getImageFile()); } else { - $model->setBaseFile($this->getProduct()->getData($model->getDestinationSubdir())); + $model->setBaseFile( + $this->getProduct() + ? $this->getProduct()->getData($model->getDestinationSubdir()) + : '' + ); } } return $this; diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml index 2eda7b8d02481..bfa80c2e24b48 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -20,8 +20,14 @@ </annotations> <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <!-- Should wait a bit for filters really cleared because waitForPageLoad does not wait for javascripts to be finished --> + <!-- Without this test will fail sometimes --> + <wait time="5" stepKey="waitFilterReallyCleared"/> + <reloadPage stepKey="reloadPage"/> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php index aa29972c91a62..c606b7537cc44 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php @@ -396,6 +396,14 @@ public function testGetWidth() $this->assertEquals($data['width'], $this->helper->getWidth()); } + /** + * Check initBaseFile without properties - product + */ + public function testGetUrlWithOutProduct() + { + $this->assertNull($this->helper->getUrl()); + } + /** * @param array $data * @dataProvider getHeightDataProvider diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php index d3c8c406ee34d..2aa30fb18fdf4 100644 --- a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php +++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php @@ -71,7 +71,7 @@ public function __construct( public function getCategoryUrlSuffix() { return $this->scopeConfig->getValue( - static::XML_PATH_CATEGORY_URL_SUFFIX, + self::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE ); } @@ -84,7 +84,7 @@ public function getCategoryUrlSuffix() public function isCategoryUsedInProductUrl(): bool { return $this->scopeConfig->isSetFlag( - static::XML_PATH_PRODUCT_USE_CATEGORIES, + self::XML_PATH_PRODUCT_USE_CATEGORIES, ScopeInterface::SCOPE_STORE ); } diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml new file mode 100644 index 0000000000000..e146506d51a24 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.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="AdminFillCatalogProductsListWidgetTitleActionGroup"> + <annotations> + <description>Fill catalog products list title field.</description> + </annotations> + + <arguments> + <argument name="title" type="string" defaultValue=""/> + </arguments> + <waitForElementVisible selector="{{InsertWidgetSection.title}}" stepKey="waitForField"/> + <fillField selector="{{InsertWidgetSection.title}}" userInput="{{title}}" stepKey="fillTitleField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml new file mode 100644 index 0000000000000..4505680424471 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.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="StorefrontAssertWidgetTitleActionGroup"> + <annotations> + <description>Assert widget title on storefront.</description> + </annotations> + <arguments> + <argument name="title" type="string"/> + </arguments> + + <grabTextFrom selector="{{StorefrontWidgetsSection.widgetProductsGrid}} {{StorefrontWidgetsSection.widgetTitle}}" + stepKey="grabWidgetTitle"/> + <assertEquals stepKey="assertWidgetTitle"> + <actualResult type="string">$grabWidgetTitle</actualResult> + <expectedResult type="string">{{title}}</expectedResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml index 9b40971611d6f..3d8d5ecc1cda9 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml @@ -19,5 +19,6 @@ <element name="checkElementStorefrontByPrice" type="button" selector="//*[@class='product-items widget-product-grid']//*[contains(text(),'${{arg4}}.00')]" parameterized="true"/> <element name="checkElementStorefrontByName" type="button" selector="//*[@class='product-items widget-product-grid']//*[@class='product-item'][{{productPosition}}]//a[contains(text(), '{{productName}}')]" parameterized="true"/> <element name="categoryTreeWrapper" type="text" selector=".rule-chooser .tree.x-tree"/> + <element name="title" type="text" selector="input[name='parameters[title]']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml new file mode 100644 index 0000000000000..28e779f802cde --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml @@ -0,0 +1,90 @@ +<?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="StorefrontCustomerCheckoutWithCustomerGroupTest"> + <annotations> + <features value="Customer Checkout"/> + <stories value="Customer checkout with Customer Group assigned"/> + <title value="Place order by Customer with Customer Group assigned"/> + <description value="Customer Group should be assigned to Order when setting Auto Group Assign is enabled for Customer"/> + <testCaseId value="MC-37259"/> + <severity value="MAJOR"/> + <group value="checkout"/> + <group value="customer"/> + </annotations> + <before> + + <magentoCLI command="config:set customer/create_account/auto_group_assign 1" stepKey="enableAutoGroupAssign"/> + + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="AdminUpdateCustomerGroupByEmailActionGroup" stepKey="updateCustomerGroup"> + <argument name="emailAddress" value="$$createCustomer.email$$"/> + <argument name="customerGroup" value="Retail"/> + </actionGroup> + + </before> + <after> + <magentoCLI command="config:set customer/create_account/auto_group_assign 0" stepKey="disableAutoGroupAssign"/> + + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteUsCustomer"/> + <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="resetCustomerFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteSimpleCategory"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="storefrontCustomerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <actionGroup ref="StorefrontAddCategoryProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="CONST.one"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> + <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="orderNumber"/> + + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="addFilterToGridAndOpenOrder"> + <argument name="orderId" value="{$orderNumber}"/> + </actionGroup> + + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="verifyOrderStatus"/> + <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="Customer" stepKey="verifyAccountInformation"/> + <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="$$createCustomer.email$$" stepKey="verifyCustomerEmail"/> + <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="Retail" stepKey="verifyCustomerGroup"/> + <see selector="{{AdminOrderDetailsInformationSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="verifyBillingAddress"/> + <see selector="{{AdminOrderDetailsInformationSection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="verifyShippingAddress"/> + <see selector="{{AdminOrderDetailsInformationSection.itemsOrdered}}" userInput="$$createSimpleProduct.name$$" stepKey="verifyProductName"/> + + </test> +</tests> diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php index 29f84e0b2e534..1f991bb47c6fd 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -10,7 +10,6 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Action\HttpPostActionInterface; -use Magento\Framework\App\Filesystem\DirectoryList; /** * Delete image folder. diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php index 82d200beb6dc9..706718455a523 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -65,7 +65,7 @@ public function execute() } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - + return $resultJson->setData($result); } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php index 9bad371aa84d7..260755ea7d562 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -74,7 +74,7 @@ public function execute() } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - + return $resultJson->setData($response); } } diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml index 7e907b5b395a4..68eca3b429e2b 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml @@ -8,6 +8,9 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminOpenCmsPageActionGroup"> + <annotations> + <description>Open CMS edit page.</description> + </annotations> <arguments> <argument name="page_id" type="string"/> </arguments> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml new file mode 100644 index 0000000000000..bc379ec424fce --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.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="StoreFrontWidgetTitleWithReservedCharsTest"> + <annotations> + <features value="Cms"/> + <stories value="Create a CMS Page via the Admin when widget title contains reserved chairs"/> + <title value="Create CMS Page via the Admin when widget title contains reserved chairs"/> + <description value="See CMS Page title on store front page if titled widget with reserved chairs added"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37419"/> + <group value="Cms"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="simpleProductWithoutCategory" stepKey="createSimpleProductWithoutCategory"/> + <createData entity="_defaultCmsPage" stepKey="createCmsPage"/> + </before> + <after> + <deleteData createDataKey="createSimpleProductWithoutCategory" stepKey="deleteProduct"/> + <deleteData createDataKey="createCmsPage" stepKey="deleteCmsPage" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Navigate to Page in Admin--> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$createCmsPage$"/> + </actionGroup> + <!--Insert widget--> + <actionGroup ref="AdminInsertWidgetToCmsPageContentActionGroup" stepKey="insertWidgetToCmsPageContent"> + <argument name="widgetType" value="Catalog Products List"/> + </actionGroup> + <!--Fill widget title and save--> + <actionGroup ref="AdminFillCatalogProductsListWidgetTitleActionGroup" stepKey="fillWidgetTitle"> + <argument name="title" value="Tittle }}"/> + </actionGroup> + <actionGroup ref="AdminClickInsertWidgetActionGroup" stepKey="clickInsertWidgetButton"/> + <actionGroup ref="SaveCmsPageActionGroup" stepKey="saveOpenedPage"/> + <!--Verify data on frontend--> + <actionGroup ref="StorefrontGoToCMSPageActionGroup" stepKey="navigateToPageOnStorefront"> + <argument name="identifier" value="$createCmsPage.identifier$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertWidgetTitleActionGroup" stepKey="verifyPageDataOnFrontend"> + <argument name="title" value="Tittle }}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index 636ff85d12e24..efd0edd23ad11 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -303,6 +303,11 @@ protected function getOptionPrices() $prices[$product->getId()] = [ + 'baseOldPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() + ), + ], 'oldPrice' => [ 'amount' => $this->localeFormat->getNumber( $priceInfo->getPrice('regular_price')->getAmount()->getValue() diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php index b8a948d55f11a..492c5de55ad7f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php @@ -39,6 +39,9 @@ public function getFormattedPrices(\Magento\Framework\Pricing\PriceInfo\Base $pr $finalPrice = $priceInfo->getPrice('final_price'); return [ + 'baseOldPrice' => [ + 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getBaseAmount()), + ], 'oldPrice' => [ 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getValue()), ], diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 33b7cbe35b391..08279c55c5b30 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -254,8 +254,11 @@ public function cacheKeyProvider(): array * @param string|null $priceCurrency * @param int|null $customerGroupId */ - public function testGetCacheKeyInfo(array $expected, ?string $priceCurrency = null, ?int $customerGroupId = null) - { + public function testGetCacheKeyInfo( + array $expected, + ?string $priceCurrency = null, + ?int $customerGroupId = null + ): void { $storeMock = $this->getMockBuilder(StoreInterface::class) ->setMethods(['getCurrentCurrency']) ->getMockForAbstractClass(); @@ -282,7 +285,7 @@ public function testGetCacheKeyInfo(array $expected, ?string $priceCurrency = nu /** * Check that getJsonConfig() method returns expected value */ - public function testGetJsonConfig() + public function testGetJsonConfig(): void { $productId = 1; $amount = 10.50; @@ -347,6 +350,9 @@ public function testGetJsonConfig() ->with($priceInfoMock) ->willReturn( [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], @@ -386,6 +392,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): 'currencyFormat' => '%s', 'optionPrices' => [ $productId => [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], @@ -403,12 +412,15 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): ], ], 'msrpPrice' => [ - 'amount' => null , + 'amount' => null, ] ], ], 'priceFormat' => [], 'prices' => [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], @@ -434,7 +446,7 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): * @param MockObject $productMock * @return MockObject */ - private function getProductTypeMock(MockObject $productMock) + private function getProductTypeMock(MockObject $productMock): MockObject { $currencyMock = $this->getMockBuilder(Currency::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php index aa546ae7ad728..c6aa9dc8e20c0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php @@ -36,9 +36,12 @@ protected function setUp(): void ); } - public function testGetFormattedPrices() + public function testGetFormattedPrices(): void { $expected = [ + 'baseOldPrice' => [ + 'amount' => 1000 + ], 'oldPrice' => [ 'amount' => 500 ], @@ -60,8 +63,8 @@ public function testGetFormattedPrices() $this->localeFormatMock->expects($this->atLeastOnce()) ->method('getNumber') - ->withConsecutive([500], [1000], [500]) - ->will($this->onConsecutiveCalls(500, 1000, 500)); + ->withConsecutive([1000], [500], [1000], [500]) + ->will($this->onConsecutiveCalls(1000, 500, 1000, 500)); $this->assertEquals($expected, $this->model->getFormattedPrices($priceInfoMock)); } diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml index eee9f742a59a4..e9d0c065fd8bf 100644 --- a/app/code/Magento/Contact/view/frontend/templates/form.phtml +++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml @@ -69,8 +69,8 @@ $viewModel = $block->getViewModel(); class="input-text" cols="5" rows="3" - data-validate="{required:true}"><?= $block->escapeHtml($viewModel->getUserComment()) ?> - </textarea> + data-validate="{required:true}" + ><?= $block->escapeHtml($viewModel->getUserComment()) ?></textarea> </div> </div> <?= $block->getChildHtml('form.additional.info') ?> diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml index f26b6feea3b3b..609b435f8b39c 100644 --- a/app/code/Magento/Cron/etc/db_schema.xml +++ b/app/code/Magento/Cron/etc/db_schema.xml @@ -28,5 +28,9 @@ <column name="scheduled_at"/> <column name="status"/> </index> + <index referenceId="CRON_SCHEDULE_SCHEDULE_ID_STATUS" indexType="btree"> + <column name="schedule_id"/> + <column name="status"/> + </index> </table> </schema> diff --git a/app/code/Magento/Cron/etc/db_schema_whitelist.json b/app/code/Magento/Cron/etc/db_schema_whitelist.json index c8666896627e2..f0d6ebed8290f 100644 --- a/app/code/Magento/Cron/etc/db_schema_whitelist.json +++ b/app/code/Magento/Cron/etc/db_schema_whitelist.json @@ -12,10 +12,11 @@ }, "index": { "CRON_SCHEDULE_JOB_CODE": true, - "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true + "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true, + "CRON_SCHEDULE_SCHEDULE_ID_STATUS": true }, "constraint": { "PRIMARY": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php index ec73ac0cf7aa5..9e7a2b69f20a5 100644 --- a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php +++ b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php @@ -41,7 +41,14 @@ protected function _prepareLayout() ] ); - $onClick = "setLocation('" . $this->getUrl('adminhtml/system_config/edit/section/currency') . "')"; + $currencyOptionPath = $this->getUrl( + 'adminhtml/system_config/edit', + [ + 'section' => 'currency', + '_fragment' => 'currency_options-link' + ] + ); + $onClick = "setLocation('$currencyOptionPath')"; $this->getToolbar()->addChild( 'options_button', diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.xml similarity index 59% rename from app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml rename to app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.xml index 0f87ee90b7ce0..39f37c745998e 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.xml @@ -5,12 +5,11 @@ * 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="AdminOpenCmsBlockActionGroup"> - <arguments> - <argument name="block_id" type="string"/> - </arguments> - <amOnPage url="{{AdminEditBlockPage.url(block_id)}}" stepKey="openEditCmsBlock"/> + <actionGroup name="AdminNavigateToCurrencyRatesOptionActionGroup"> + <click selector="{{AdminCurrencyRatesSection.options}}" stepKey="clickOptionsButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml index bc80a51c41c47..10f345ec69369 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml @@ -11,6 +11,7 @@ <section name="AdminCurrencyRatesSection"> <element name="import" type="button" selector="//button[@title='Import']"/> <element name="saveCurrencyRates" type="button" selector="//button[@title='Save Currency Rates']"/> + <element name="options" type="button" selector="//button[@title='Options']"/> <element name="oldRate" type="text" selector="//div[contains(@class, 'admin__field-note') and contains(text(), 'Old rate:')]/strong"/> <element name="rateService" type="select" selector="#rate_services"/> <element name="currencyRate" type="input" selector="input[name='rate[{{fistCurrency}}][{{secondCurrency}}]']" parameterized="true"/> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml new file mode 100644 index 0000000000000..4e0eb72df3aa5 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml @@ -0,0 +1,37 @@ +<?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="AdminCurrencyOptionsSystemConfigExpandedTabTest"> + <annotations> + <features value="Expanded tab on Currency Option page"/> + <stories value="Expanded tab"/> + <title value=" Verify the Currency Option tab expands automatically."/> + <description value="Check auto open the collapse on Currency Option page."/> + <severity value="MINOR"/> + <testCaseId value="MC-37425"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresCurrencyRatesPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencyRates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminNavigateToCurrencyRatesOptionActionGroup" stepKey="navigateToOptions" /> + <grabAttributeFrom selector="{{CurrencySetupSection.currencyOptions}}" userInput="class" stepKey="grabClass"/> + <assertStringContainsString stepKey="assertClass"> + <actualResult type="string">{$grabClass}</actualResult> + <expectedResult type="string">open</expectedResult> + </assertStringContainsString> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php index aa7cd06666121..4b86df94b4556 100644 --- a/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php @@ -7,15 +7,22 @@ namespace Magento\CurrencySymbol\Test\Unit\Block\Adminhtml\System; +use Magento\Backend\Block\Template\Context; use Magento\Backend\Block\Widget\Button; use Magento\CurrencySymbol\Block\Adminhtml\System\Currency; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Element\BlockInterface; use Magento\Framework\View\LayoutInterface; use PHPUnit\Framework\TestCase; +use Magento\Framework\UrlInterface; class CurrencyTest extends TestCase { + /** + * Stub currency option link url + */ + const STUB_OPTION_LINK_URL = 'https://localhost/admin/system_config/edit/section/currency#currency_options-link'; + /** * Object manager helper * @@ -70,12 +77,25 @@ public function testPrepareLayout() ] ); + $contextMock = $this->createMock(Context::class); + $urlBuilderMock = $this->createMock(UrlInterface::class); + + $contextMock->expects($this->once())->method('getUrlBuilder')->willReturn($urlBuilderMock); + + $urlBuilderMock->expects($this->once())->method('getUrl')->with( + 'adminhtml/system_config/edit', + [ + 'section' => 'currency', + '_fragment' => 'currency_options-link' + ] + )->willReturn(self::STUB_OPTION_LINK_URL); + $childBlockMock->expects($this->at(1)) ->method('addChild') ->with( 'options_button', Button::class, - ['label' => __('Options'), 'onclick' => 'setLocation(\'\')'] + ['label' => __('Options'), 'onclick' => 'setLocation(\''.self::STUB_OPTION_LINK_URL.'\')'] ); $childBlockMock->expects($this->at(2)) @@ -90,7 +110,8 @@ public function testPrepareLayout() $block = $this->objectManagerHelper->getObject( Currency::class, [ - 'layout' => $layoutMock + 'layout' => $layoutMock, + 'context' => $contextMock ] ); $block->setLayout($layoutMock); diff --git a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml index a16a3aae14b49..a377cd8ae6722 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -135,7 +135,7 @@ require([ content: "{$block->escapeJs(__('Are you sure you want to strip tags?'))}", actions: { confirm: function () { - this.unconvertedText = $('template_text').value; + self.unconvertedText = $('template_text').value; $('convert_button').hide(); $('template_text').value = $('template_text').value.stripScripts().replace( new RegExp('<style[^>]*>[\\S\\s]*?</style>', 'img'), '' diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php index bcce3514e4ad9..b01c02cae4234 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -7,6 +7,11 @@ namespace Magento\MediaContentSynchronization\Model; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; /** @@ -14,24 +19,73 @@ */ class Consume { + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + /** * @var SynchronizeInterface */ private $synchronize; /** + * @var SynchronizeIdentitiesInterface + */ + private $synchronizeIdentities; + + /** + * @param SerializerInterface $serializer + * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param SynchronizeInterface $synchronize + * @param SynchronizeIdentitiesInterface $synchronizeIdentities */ - public function __construct(SynchronizeInterface $synchronize) - { + public function __construct( + SerializerInterface $serializer, + ContentIdentityInterfaceFactory $contentIdentityFactory, + SynchronizeInterface $synchronize, + SynchronizeIdentitiesInterface $synchronizeIdentities + ) { + $this->serializer = $serializer; + $this->contentIdentityFactory = $contentIdentityFactory; $this->synchronize = $synchronize; + $this->synchronizeIdentities = $synchronizeIdentities; } /** * Run media files synchronization. + * + * @param OperationInterface $operation + * @throws LocalizedException */ - public function execute() : void + public function execute(OperationInterface $operation) : void { - $this->synchronize->execute(); + $identities = $this->serializer->unserialize($operation->getSerializedData()); + + if (empty($identities)) { + $this->synchronize->execute(); + return; + } + + $contentIdentities = []; + foreach ($identities as $identity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $identity[self::ENTITY_TYPE], + self::ENTITY_ID => $identity[self::ENTITY_ID], + self::FIELD => $identity[self::FIELD] + ] + ); + } + $this->synchronizeIdentities->execute($contentIdentities); } } diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php index ad6fdd27d7067..d9e89fea7d4d2 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Publish.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -7,7 +7,11 @@ namespace Magento\MediaContentSynchronization\Model; +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\Framework\Bulk\OperationInterface; +use Magento\Framework\DataObject\IdentityGeneratorInterface; use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\Framework\Serialize\SerializerInterface; /** * Publish media content synchronization queue. @@ -19,27 +23,64 @@ class Publish */ private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization'; + /** + * @var OperationInterfaceFactory + */ + private $operationFactory; + + /** + * @var IdentityGeneratorInterface + */ + private $identityService; + /** * @var PublisherInterface */ private $publisher; /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @param OperationInterfaceFactory $operationFactory + * @param IdentityGeneratorInterface $identityService * @param PublisherInterface $publisher + * @param SerializerInterface $serializer */ - public function __construct(PublisherInterface $publisher) - { + public function __construct( + OperationInterfaceFactory $operationFactory, + IdentityGeneratorInterface $identityService, + PublisherInterface $publisher, + SerializerInterface $serializer + ) { + $this->operationFactory = $operationFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; $this->publisher = $publisher; } /** - * Publish media content synchronization message to the message queue. + * Publish media content synchronization message to the message queue + * + * @param array $contentIdentities */ - public function execute() : void + public function execute(array $contentIdentities = []) : void { + $data = [ + 'data' => [ + 'bulk_uuid' => $this->identityService->generateId(), + 'topic_name' => self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, + 'serialized_data' => $this->serializer->serialize($contentIdentities), + 'status' => OperationInterface::STATUS_TYPE_OPEN, + ] + ]; + $operation = $this->operationFactory->create($data); + $this->publisher->publish( self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, - [self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION] + $operation ); } } diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php new file mode 100644 index 0000000000000..1bf57c6b2ec42 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\FlagManager; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; +use Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool; +use Psr\Log\LoggerInterface; + +/** + * Batch Synchronize content with assets + */ +class SynchronizeIdentities implements SynchronizeIdentitiesInterface +{ + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var SynchronizeIdentitiesPool + */ + private $synchronizeIdentitiesPool; + + /** + * @param LoggerInterface $log + * @param SynchronizeIdentitiesPool $synchronizeIdentitiesPool + */ + public function __construct( + LoggerInterface $log, + SynchronizeIdentitiesPool $synchronizeIdentitiesPool + ) { + $this->log = $log; + $this->synchronizeIdentitiesPool = $synchronizeIdentitiesPool; + } + + /** + * @inheritdoc + */ + public function execute(array $mediaContentIdentities): void + { + $failed = []; + + foreach ($this->synchronizeIdentitiesPool->get() as $name => $synchronizer) { + try { + $synchronizer->execute($mediaContentIdentities); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following content synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php new file mode 100644 index 0000000000000..2314796481b55 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Test\Integration\Model; + +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\MessageQueue\ConsumerFactory; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronization\Model\Publish; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for media content Publisher + */ +class PublisherTest extends TestCase +{ + private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization'; + + /** + * @var ConsumerFactory + */ + private $consumerFactory; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var Publish + */ + private $publish; + + protected function setUp(): void + { + $this->consumerFactory = Bootstrap::getObjectManager()->get(ConsumerFactory::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->publish = Bootstrap::getObjectManager()->get(Publish::class); + } + + /** + * @dataProvider filesProvider + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @param array $contentIdentities + * @throws IntegrationException + * @throws LocalizedException + */ + public function testExecute(array $contentIdentities): void + { + // publish message to the queue + $this->publish->execute($contentIdentities); + + // run and process message + $batchSize = 1; + $maxNumberOfMessages = 1; + $consumer = $this->consumerFactory->get(self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, $batchSize); + $consumer->process($maxNumberOfMessages); + + // verify synchronized media content + $assetId = 2020; + $entityIds = []; + foreach ($contentIdentities as $contentIdentity) { + $contentIdentityObject = $this->contentIdentityFactory->create($contentIdentity); + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentityObject)); + $entityIds[] = $contentIdentityObject->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); + } + } + + /** + * Data provider + * + * @return array + */ + public function filesProvider(): array + { + return [ + [ + [ + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => 28767 + ], + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => 1567 + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json index 3be5f535487ec..9f0f4f9588ad6 100644 --- a/app/code/Magento/MediaContentSynchronization/composer.json +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -4,9 +4,10 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/framework-bulk": "*", "magento/module-media-content-synchronization-api": "*", - "magento/framework-message-queue": "*", - "magento/module-media-content-api": "*" + "magento/module-media-content-api": "*", + "magento/module-asynchronous-operations": "*" }, "suggest": { "magento/module-media-gallery-synchronization": "*" diff --git a/app/code/Magento/MediaContentSynchronization/etc/communication.xml b/app/code/Magento/MediaContentSynchronization/etc/communication.xml index e3436aee85331..05641b7432564 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/communication.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/communication.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> - <topic name="media.content.synchronization" is_synchronous="false" request="string[]"> + <topic name="media.content.synchronization" is_synchronous="false" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> <handler name="media.content.synchronization.handler" type="Magento\MediaContentSynchronization\Model\Consume" method="execute"/> </topic> diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml index d4615c15206e5..e5347f1a11561 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface" type="Magento\MediaContentSynchronization\Model\Synchronize"/> + <preference for="Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface" type="Magento\MediaContentSynchronization\Model\SynchronizeIdentities"/> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> diff --git a/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php new file mode 100644 index 0000000000000..7e21cbb570053 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Api; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; + +/** + * Synchronize bulk assets and contents + */ +interface SynchronizeIdentitiesInterface +{ + /** + * Synchronize media contents + * + * @param ContentIdentityInterface[] $contentIdentities + */ + public function execute(array $contentIdentities): void; +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php new file mode 100644 index 0000000000000..1ea957d5cd6e7 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Model; + +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; + +class SynchronizeIdentitiesPool +{ + /** + * Content with assets synchronizers + * + * @var SynchronizeIdentitiesInterface[] + */ + private $synchronizers; + + /** + * @param SynchronizeIdentitiesInterface[] $synchronizers + */ + public function __construct( + array $synchronizers = [] + ) { + foreach ($synchronizers as $synchronizer) { + if (!$synchronizer instanceof SynchronizeIdentitiesInterface) { + throw new \InvalidArgumentException( + get_class($synchronizer) . ' must implement ' . SynchronizeIdentitiesInterface::class + ); + } + } + + $this->synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizeIdentitiesInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json index 1f1e5e4b51c5b..398aaf1de8071 100644 --- a/app/code/Magento/MediaContentSynchronizationApi/composer.json +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -3,7 +3,8 @@ "description": "Magento module responsible for the media content synchronization implementation API", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-media-content-api": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php new file mode 100644 index 0000000000000..77188b65a8b88 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Model\Synchronizer; + +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; + +class SynchronizeIdentities implements SynchronizeIdentitiesInterface +{ + private const FIELD_CATALOG_PRODUCT = 'catalog_product'; + private const FIELD_CATALOG_CATEGORY = 'catalog_category'; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param GetEntityContentsInterface $getEntityContents + */ + public function __construct( + UpdateContentAssetLinksInterface $updateContentAssetLinks, + GetEntityContentsInterface $getEntityContents + ) { + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->getEntityContents = $getEntityContents; + } + + /** + * @inheritDoc + */ + public function execute(array $mediaContentIdentities): void + { + foreach ($mediaContentIdentities as $identity) { + if ($identity->getEntityType() === self::FIELD_CATALOG_PRODUCT + || $identity->getEntityType() === self::FIELD_CATALOG_CATEGORY + ) { + $this->updateContentAssetLinks->execute( + $identity, + implode(PHP_EOL, $this->getEntityContents->execute($identity)) + ); + } + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php new file mode 100644 index 0000000000000..5be72e2b4bf60 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Test\Integration\Model\Synchronizer; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for catalog SynchronizeIdentities. + */ +class SynchronizeIdentitiesTest extends TestCase +{ + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var SynchronizeIdentitiesInterface + */ + private $synchronizeIdentities; + + protected function setUp(): void + { + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + } + + /** + * @dataProvider filesProvider + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @param ContentIdentityInterface[] $mediaContentIdentities + * @throws IntegrationException + */ + public function testExecute(array $mediaContentIdentities): void + { + $assetId = 2020; + + $contentIdentities = []; + foreach ($mediaContentIdentities as $mediaContentIdentity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $mediaContentIdentity[self::ENTITY_TYPE], + self::ENTITY_ID => $mediaContentIdentity[self::ENTITY_ID], + self::FIELD => $mediaContentIdentity[self::FIELD] + ] + ); + } + + $this->assertNotEmpty($contentIdentities); + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->synchronizeIdentities->execute($contentIdentities); + + $entityIds = []; + foreach ($contentIdentities as $contentIdentity) { + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + $entityIds[] = $contentIdentity->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); + } + } + + /** + * Data provider + * + * @return array + */ + public function filesProvider(): array + { + return [ + [ + [ + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => 28767 + ], + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => 1567 + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml index 8cc86fde8fbcd..070f25f501712 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -38,4 +38,13 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_catalog" + xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\SynchronizeIdentities + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php new file mode 100644 index 0000000000000..7dd2596a910de --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Model\Synchronizer; + +use Magento\Framework\App\ResourceConnection; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; + +class SynchronizeIdentities implements SynchronizeIdentitiesInterface +{ + private const FIELD_CMS_PAGE = 'cms_page'; + private const FIELD_CMS_BLOCK = 'cms_block'; + private const ID_CMS_PAGE = 'page_id'; + private const ID_CMS_BLOCK = 'block_id'; + private const COLUMN_CMS_CONTENT = 'content'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @param ResourceConnection $resourceConnection + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param GetEntityContentsInterface $getEntityContents + */ + public function __construct( + ResourceConnection $resourceConnection, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + GetEntityContentsInterface $getEntityContents + ) { + $this->resourceConnection = $resourceConnection; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->getEntityContents = $getEntityContents; + } + + /** + * @inheritDoc + */ + public function execute(array $mediaContentIdentities): void + { + foreach ($mediaContentIdentities as $identity) { + if ($identity->getEntityType() === self::FIELD_CMS_PAGE + || $identity->getEntityType() === self::FIELD_CMS_BLOCK + ) { + $this->updateContentAssetLinks->execute( + $identity, + $this->getCmsMediaContent($identity->getEntityType(), (int)$identity->getEntityId()) + ); + } + } + } + + /** + * Get cms media content from database + * + * @param string $tableName + * @param int $cmsId + * @return string + */ + private function getCmsMediaContent(string $tableName, int $cmsId): string + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName($tableName); + $idField = $tableName == self::FIELD_CMS_BLOCK ? $idField = self::ID_CMS_BLOCK : self::ID_CMS_PAGE; + + $select = $connection->select() + ->from($tableName, self::COLUMN_CMS_CONTENT) + ->where($idField . '= ?', $cmsId); + $data = $connection->fetchOne($select); + + return (string)$data; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php new file mode 100644 index 0000000000000..825542baaff8c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Test\Integration\Model\Synchronizer; + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for CMS SynchronizeIdentities. + */ +class SynchronizeIdentitiesTest extends TestCase +{ + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var SynchronizeIdentitiesInterface + */ + private $synchronizeIdentities; + + protected function setUp(): void + { + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + } + + /** + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @throws IntegrationException + * @throws LocalizedException + */ + public function testExecute(): void + { + $assetId = 2020; + $pageId = $this->getPage('fixture_page_with_asset')->getId(); + $blockId = $this->getBlock('fixture_block_with_asset')->getId(); + $mediaContentIdentities = [ + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => $pageId + ], + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => $blockId + ] + ]; + + $contentIdentities = []; + foreach ($mediaContentIdentities as $mediaContentIdentity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $mediaContentIdentity[self::ENTITY_TYPE], + self::ENTITY_ID => $mediaContentIdentity[self::ENTITY_ID], + self::FIELD => $mediaContentIdentity[self::FIELD] + ] + ); + } + + $this->assertNotEmpty($contentIdentities); + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->synchronizeIdentities->execute($contentIdentities); + + $entityIds = []; + foreach ($contentIdentities as $contentIdentity) { + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + $entityIds[] = $contentIdentity->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); + } + } + + /** + * Get fixture block + * + * @param string $identifier + * @return BlockInterface + * @throws LocalizedException + */ + private function getBlock(string $identifier): BlockInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var BlockRepositoryInterface $blockRepository */ + $blockRepository = $objectManager->get(BlockRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, $identifier) + ->create(); + + return current($blockRepository->getList($searchCriteria)->getItems()); + } + + /** + * Get fixture page + * + * @param string $identifier + * @return PageInterface + * @throws LocalizedException + */ + private function getPage(string $identifier): PageInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var PageRepositoryInterface $repository */ + $repository = $objectManager->get(PageRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, $identifier) + ->create(); + + return current($repository->getList($searchCriteria)->getItems()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml index 7def330298789..d6e7604c71d97 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -14,6 +14,15 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_cms" + xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\SynchronizeIdentities + </item> + </argument> + </arguments> + </type> <type name="Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface"> <arguments> <argument name="entities" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php index d439b53c120cb..b683ec8fe9d91 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php @@ -81,17 +81,18 @@ public function __construct( * * @param ImageUploader $subject * @param string $imagePath + * @param string $initialImageName * @return string * @throws LocalizedException */ - public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath): string + public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath, string $initialImageName): string { if (!$this->config->isEnabled()) { return $imagePath; } $absolutePath = $this->storage->getCmsWysiwygImages()->getStorageRoot() . $imagePath; - $tmpPath = $subject->getBaseTmpPath() . '/' . substr(strrchr($imagePath, '/'), 1); + $tmpPath = $subject->getBaseTmpPath() . '/' . $initialImageName; $tmpAssets = $this->getAssetsByPaths->execute([$tmpPath]); if (!empty($tmpAssets)) { diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml new file mode 100644 index 0000000000000..8add2021f056b --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml @@ -0,0 +1,50 @@ +<?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="AdminUploadSameImageDeleteFromTemporaryFolderTest"> + <annotations> + <features value="AdminUploadSameImageDeleteFromTemporaryFolderTest"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1792"/> + <title value="Image is deleted from tmp folder if is uploaded second time"/> + <description value="Image is deleted from tmp folder if is uploaded second time"/> + <stories value="Image is deleted from tmp folder if is uploaded second time"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4836631"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + + <!-- Upload test image to category twice --> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImageSecondTime"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryFormSecondTime"/> + + <!-- Open tmp/category folder --> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup" stepKey="expandTmpFolder"/> + <actionGroup ref="AdminMediaGalleryFolderSelectByFullPathActionGroup" stepKey="selectCategoryFolder"> + <argument name="path" value="catalog/tmp/category"/> + </actionGroup> + + <!-- Assert folder is empty --> + <actionGroup ref="AdminAssertMediaGalleryEmptyActionGroup" stepKey="assertEmptyFolder"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml index de8517eedae0e..5a375d9153a6d 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryAssertUsedInLinkPagesGridTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> <features value="AdminMediaGalleryUsedInBlocksFilter"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> <title value="Used in pages link"/> @@ -21,10 +24,6 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> - <after> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> - </after> - <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToCreateNewPage"/> <actionGroup ref="FillOutCustomCMSPageContentActionGroup" stepKey="fillBasicPageDataForPageWithDefaultStore"> <argument name="title" value="Unique page title MediaGalleryUi"/> @@ -37,9 +36,13 @@ <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> <argument name="image" value="ImageUpload3"/> </actionGroup> - <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> - <argument name="imageName" value="{{ImageMetadata.title}}"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="savePage"/> <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> @@ -48,13 +51,11 @@ <argument name="entityName" value="Pages"/> </actionGroup> <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> - <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> </actionGroup> - <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> <argument name="urlKey" value="test-page-1"/> </actionGroup> - <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerfifyEmptyUsedIn"/> <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> @@ -62,7 +63,7 @@ <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> - <argument name="imageName" value="{{ImageMetadata.title}}"/> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml index 038f1ae077b4a..e72e65cf8de90 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryCmsUiUsedInPagesFilterTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> <features value="AdminMediaGalleryUsedInPagesFilter"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> <title value="Used in pages filter"/> diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Config.php b/app/code/Magento/MediaGalleryRenditions/Model/Config.php new file mode 100644 index 0000000000000..d1a48904d1f13 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Config.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Class responsible for providing access to Media Gallery Renditions system configuration. + */ +class Config +{ + private const TABLE_CORE_CONFIG_DATA = 'core_config_data'; + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + ResourceConnection $resourceConnection + ) { + $this->scopeConfig = $scopeConfig; + $this->resourceConnection = $resourceConnection; + } + + /** + * Check if the media gallery is enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } + + /** + * Get max width + * + * @return int + */ + public function getWidth(): int + { + try { + return $this->getDatabaseValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); + } catch (NoSuchEntityException $exception) { + return (int) $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); + } + } + + /** + * Get max height + * + * @return int + */ + public function getHeight(): int + { + try { + return $this->getDatabaseValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); + } catch (NoSuchEntityException $exception) { + return (int) $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); + } + } + + /** + * Get value from database bypassing config cache + * + * @param string $path + * @return int + * @throws NoSuchEntityException + */ + private function getDatabaseValue(string $path): int + { + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from( + [ + 'config' => $this->resourceConnection->getTableName(self::TABLE_CORE_CONFIG_DATA) + ], + [ + 'value' + ] + ) + ->where('config.path = ?', $path); + $value = $connection->query($select)->fetchColumn(); + + if ($value === false) { + throw new NoSuchEntityException( + __( + 'The config value for %path is not saved to database.', + ['path' => $path] + ) + ); + } + + return (int) $value; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php new file mode 100644 index 0000000000000..6bc54fdf9aca4 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Image\AdapterFactory; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +class GenerateRenditions implements GenerateRenditionsInterface +{ + private const IMAGE_FILE_NAME_PATTERN = '#\.(jpg|jpeg|gif|png)$# i'; + + /** + * @var AdapterFactory + */ + private $imageFactory; + + /** + * @var Config + */ + private $config; + + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var File + */ + private $driver; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param AdapterFactory $imageFactory + * @param Config $config + * @param GetRenditionPathInterface $getRenditionPath + * @param Filesystem $filesystem + * @param File $driver + * @param IsPathExcludedInterface $isPathExcluded + * @param LoggerInterface $log + */ + public function __construct( + AdapterFactory $imageFactory, + Config $config, + GetRenditionPathInterface $getRenditionPath, + Filesystem $filesystem, + File $driver, + IsPathExcludedInterface $isPathExcluded, + LoggerInterface $log + ) { + $this->imageFactory = $imageFactory; + $this->config = $config; + $this->getRenditionPath = $getRenditionPath; + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->isPathExcluded = $isPathExcluded; + $this->log = $log; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $failedPaths = []; + + foreach ($paths as $path) { + try { + $this->generateRendition($path); + } catch (\Exception $exception) { + $this->log->error($exception); + $failedPaths[] = $path; + } + } + + if (!empty($failedPaths)) { + throw new LocalizedException( + __( + 'Cannot create rendition for media asset paths: %paths', + [ + 'paths' => implode(', ', $failedPaths) + ] + ) + ); + } + } + + /** + * Generate rendition for media asset path + * + * @param string $path + * @throws FileSystemException + * @throws LocalizedException + * @throws \Exception + */ + private function generateRendition(string $path): void + { + $this->validateAsset($path); + + $renditionPath = $this->getRenditionPath->execute($path); + $this->createDirectory($renditionPath); + + $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); + + if ($this->shouldFileBeResized($absolutePath)) { + $this->createResizedRendition( + $absolutePath, + $this->getMediaDirectory()->getAbsolutePath($renditionPath) + ); + } else { + $this->getMediaDirectory()->copyFile($path, $renditionPath); + } + } + + /** + * Ensure valid media asset path is provided for renditions generation + * + * @param string $path + * @throws FileSystemException + * @throws LocalizedException + */ + private function validateAsset(string $path): void + { + if (!$this->getMediaDirectory()->isFile($path)) { + throw new LocalizedException(__('Media asset file %path does not exist!', ['path' => $path])); + } + + if ($this->isPathExcluded->execute($path)) { + throw new LocalizedException( + __('Could not create rendition for image, path is restricted: %path', ['path' => $path]) + ); + } + + if (!preg_match(self::IMAGE_FILE_NAME_PATTERN, $path)) { + throw new LocalizedException( + __('Could not create rendition for image, unsupported file type: %path.', ['path' => $path]) + ); + } + } + + /** + * Create directory for rendition file + * + * @param string $path + * @throws LocalizedException + */ + private function createDirectory(string $path): void + { + try { + $this->getMediaDirectory()->create($this->driver->getParentDirectory($path)); + } catch (\Exception $exception) { + throw new LocalizedException(__('Cannot create directory for rendition %path', ['path' => $path])); + } + } + + /** + * Create rendition file + * + * @param string $absolutePath + * @param string $absoluteRenditionPath + * @throws \Exception + */ + private function createResizedRendition(string $absolutePath, string $absoluteRenditionPath): void + { + $image = $this->imageFactory->create(); + $image->open($absolutePath); + $image->keepAspectRatio(true); + $image->resize($this->config->getWidth(), $this->config->getHeight()); + $image->save($absoluteRenditionPath); + } + + /** + * Check if image needs to resize or not + * + * @param string $absolutePath + * @return bool + */ + private function shouldFileBeResized(string $absolutePath): bool + { + [$width, $height] = getimagesize($absolutePath); + return $width > $this->config->getWidth() || $height > $this->config->getHeight(); + } + + /** + * Retrieve a media directory instance with write permissions + * + * @return WriteInterface + * @throws FileSystemException + */ + private function getMediaDirectory(): WriteInterface + { + return $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php b/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php new file mode 100644 index 0000000000000..1c93141429ab0 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; + +class GetRenditionPath implements GetRenditionPathInterface +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * Returns Rendition image path + * + * @param string $path + * @return string + */ + public function execute(string $path): string + { + return self::RENDITIONS_DIRECTORY_NAME . '/' . ltrim($path, '/'); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php new file mode 100644 index 0000000000000..7263010a8f587 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Psr\Log\LoggerInterface; + +/** + * Fetch files from media storage in batches + */ +class FetchRenditionPathsBatches +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * @var GetFilesIterator + */ + private $getFilesIterator; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $fileExtensions; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var int + */ + private $batchSize; + + /** + * @param LoggerInterface $log + * @param Filesystem $filesystem + * @param GetFilesIterator $getFilesIterator + * @param int $batchSize + * @param array $fileExtensions + */ + public function __construct( + LoggerInterface $log, + Filesystem $filesystem, + GetFilesIterator $getFilesIterator, + int $batchSize, + array $fileExtensions + ) { + $this->log = $log; + $this->getFilesIterator = $getFilesIterator; + $this->filesystem = $filesystem; + $this->batchSize = $batchSize; + $this->fileExtensions = $fileExtensions; + } + + /** + * Return files from files system by provided size of batch + */ + public function execute(): \Traversable + { + $index = 0; + $batch = []; + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + $iterator = $this->getFilesIterator->execute( + $mediaDirectory->getAbsolutePath(self::RENDITIONS_DIRECTORY_NAME) + ); + + /** @var \SplFileInfo $file */ + foreach ($iterator as $file) { + $relativePath = $mediaDirectory->getRelativePath($file->getPathName()); + if (!$this->isApplicable($relativePath)) { + continue; + } + + $batch[] = $relativePath; + if (++$index == $this->batchSize) { + yield $batch; + $index = 0; + $batch = []; + } + } + if (count($batch) > 0) { + yield $batch; + } + } + + /** + * Is the path a valid image path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path && preg_match('#\.(' . implode("|", $this->fileExtensions) . ')$# i', $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php new file mode 100644 index 0000000000000..97efcdc81ba50 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +/** + * Retrieve files iterator for path + */ +class GetFilesIterator +{ + /** + * Get files iterator for provided path + * + * @param string $path + * @return \RecursiveIteratorIterator + */ + public function execute(string $path): \RecursiveIteratorIterator + { + return new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $path, + \FilesystemIterator::SKIP_DOTS | + \FilesystemIterator::UNIX_PATHS | + \RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php new file mode 100644 index 0000000000000..051c883025587 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\MessageQueue\PublisherInterface; + +/** + * Publish media gallery renditions update message to the queue. + */ +class ScheduleRenditionsUpdate +{ + private const TOPIC_MEDIA_GALLERY_UPDATE_RENDITIONS = 'media.gallery.renditions.update'; + + /** + * @var PublisherInterface + */ + private $publisher; + + /** + * @param PublisherInterface $publisher + */ + public function __construct(PublisherInterface $publisher) + { + $this->publisher = $publisher; + } + + /** + * Publish media gallery renditions update message to the queue. + * + * @param array $paths + */ + public function execute(array $paths = []): void + { + $this->publisher->publish( + self::TOPIC_MEDIA_GALLERY_UPDATE_RENDITIONS, + $paths + ); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php new file mode 100644 index 0000000000000..45cea58d05018 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Psr\Log\LoggerInterface; + +/** + * Renditions update queue consumer. + */ +class UpdateRenditions +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var FetchRenditionPathsBatches + */ + private $fetchRenditionPathsBatches; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GenerateRenditionsInterface $generateRenditions + * @param FetchRenditionPathsBatches $fetchRenditionPathsBatches + * @param LoggerInterface $log + */ + public function __construct( + GenerateRenditionsInterface $generateRenditions, + FetchRenditionPathsBatches $fetchRenditionPathsBatches, + LoggerInterface $log + ) { + $this->generateRenditions = $generateRenditions; + $this->fetchRenditionPathsBatches = $fetchRenditionPathsBatches; + $this->log = $log; + } + + /** + * Update renditions for given paths, if empty array is provided - all renditions are updated + * + * @param array $paths + * @throws LocalizedException + */ + public function execute(array $paths): void + { + if (!empty($paths)) { + $this->updateRenditions($paths); + return; + } + + foreach ($this->fetchRenditionPathsBatches->execute() as $renditionPaths) { + $this->updateRenditions($renditionPaths); + } + } + + /** + * Update renditions and log exceptions + * + * @param string[] $renditionPaths + */ + private function updateRenditions(array $renditionPaths): void + { + try { + $this->generateRenditions->execute($this->getAssetPaths($renditionPaths)); + } catch (LocalizedException $exception) { + $this->log->error($exception); + } + } + + /** + * Get asset paths based on rendition paths + * + * @param string[] $renditionPaths + * @return string[] + */ + private function getAssetPaths(array $renditionPaths): array + { + $paths = []; + + foreach ($renditionPaths as $renditionPath) { + try { + $paths[] = $this->getAssetPath($renditionPath); + } catch (\Exception $exception) { + $this->log->error($exception); + } + } + + return $paths; + } + + /** + * Get asset path based on rendition path + * + * @param string $renditionPath + * @return string + * @throws LocalizedException + */ + private function getAssetPath(string $renditionPath): string + { + if (strpos($renditionPath, self::RENDITIONS_DIRECTORY_NAME) !== 0) { + throw new LocalizedException( + __( + 'Incorrect rendition path provided for update: %path', + [ + 'path' => $renditionPath + ] + ) + ); + } + + return substr($renditionPath, strlen(self::RENDITIONS_DIRECTORY_NAME)); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php new file mode 100644 index 0000000000000..f0ba8c3533722 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +/** + * Remove renditions when assets are removed + */ +class RemoveRenditions +{ + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GetRenditionPathInterface $getRenditionPath + * @param Filesystem $filesystem + * @param LoggerInterface $log + */ + public function __construct( + GetRenditionPathInterface $getRenditionPath, + Filesystem $filesystem, + LoggerInterface $log + ) { + $this->getRenditionPath = $getRenditionPath; + $this->filesystem = $filesystem; + $this->log = $log; + } + + /** + * Remove renditions when assets are removed + * + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param void $result + * @param array $paths + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute( + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + $result, + array $paths + ): void { + $this->removeRenditions($paths); + } + + /** + * Remove rendition files + * + * @param array $paths + */ + private function removeRenditions(array $paths): void + { + foreach ($paths as $path) { + try { + $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA)->delete( + $this->getRenditionPath->execute($path) + ); + } catch (\Exception $exception) { + $this->log->error($exception); + } + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php b/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php new file mode 100644 index 0000000000000..ec2012c528ef1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\GetInsertImageContent; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryRenditions\Model\Config; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +/** + * Intercept and set renditions path on PrepareImage + */ +class SetRenditionPath +{ + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var Images + */ + private $imagesHelper; + + /** + * @var Config + */ + private $config; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GetRenditionPathInterface $getRenditionPath + * @param GenerateRenditionsInterface $generateRenditions + * @param Images $imagesHelper + * @param Config $config + * @param LoggerInterface $log + */ + public function __construct( + GetRenditionPathInterface $getRenditionPath, + GenerateRenditionsInterface $generateRenditions, + Images $imagesHelper, + Config $config, + LoggerInterface $log + ) { + $this->getRenditionPath = $getRenditionPath; + $this->generateRenditions = $generateRenditions; + $this->imagesHelper = $imagesHelper; + $this->config = $config; + $this->log = $log; + } + + /** + * Replace the original asset path with rendition path + * + * @param GetInsertImageContent $subject + * @param string $encodedFilename + * @param bool $forceStaticPath + * @param bool $renderAsTag + * @param int|null $storeId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute( + GetInsertImageContent $subject, + string $encodedFilename, + bool $forceStaticPath, + bool $renderAsTag, + ?int $storeId = null + ): array { + $arguments = [ + $encodedFilename, + $forceStaticPath, + $renderAsTag, + $storeId + ]; + + if (!$this->config->isEnabled()) { + return $arguments; + } + + $path = $this->imagesHelper->idDecode($encodedFilename); + + try { + $this->generateRenditions->execute([$path]); + } catch (LocalizedException $exception) { + $this->log->error($exception); + return $arguments; + } + + $arguments[0] = $this->imagesHelper->idEncode($this->getRenditionPath->execute($path)); + + return $arguments; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php new file mode 100644 index 0000000000000..9cf969c16782f --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Framework\App\Config\Value; +use Magento\MediaGalleryRenditions\Model\Queue\ScheduleRenditionsUpdate; + +/** + * Update renditions if corresponding configuration changes + */ +class UpdateRenditionsOnConfigChange +{ + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; + + /** + * @var ScheduleRenditionsUpdate + */ + private $scheduleRenditionsUpdate; + + /** + * @param ScheduleRenditionsUpdate $scheduleRenditionsUpdate + */ + public function __construct(ScheduleRenditionsUpdate $scheduleRenditionsUpdate) + { + $this->scheduleRenditionsUpdate = $scheduleRenditionsUpdate; + } + + /** + * Update renditions when configuration is changed + * + * @param Value $config + * @param Value $result + * @return Value + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Value $config, Value $result): Value + { + if ($this->isRenditionsValue($result) && $result->isValueChanged()) { + $this->scheduleRenditionsUpdate->execute(); + } + + return $result; + } + + /** + * Does configuration value relates to renditions + * + * @param Value $value + * @return bool + */ + private function isRenditionsValue(Value $value): bool + { + return $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH + || $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/README.md b/app/code/Magento/MediaGalleryRenditions/README.md new file mode 100644 index 0000000000000..df856e8003a84 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditions module + +The Magento_MediaGalleryRenditions module implements height and width fields for for media gallery items. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php new file mode 100644 index 0000000000000..05bb01b9ff433 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for Extracting assets from rendition paths/urls in content + */ +class ExtractAssetsFromContentWithRenditionTest extends TestCase +{ + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->extractAssetsFromContent = Bootstrap::getObjectManager() + ->get(ExtractAssetsFromContentInterface::class); + } + + /** + * Assert rendition urls/path in the content are associated with an asset + * + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * + * @dataProvider contentProvider + * @param string $content + * @param array $assetIds + */ + public function testExecute(string $content, array $assetIds): void + { + $assets = $this->extractAssetsFromContent->execute($content); + + $extractedAssetIds = []; + foreach ($assets as $asset) { + $extractedAssetIds[] = $asset->getId(); + } + + sort($assetIds); + sort($extractedAssetIds); + + $this->assertEquals($assetIds, $extractedAssetIds); + } + + /** + * Data provider for testExecute + * + * @return array + */ + public function contentProvider() + { + return [ + 'Empty Content' => [ + '', + [] + ], + 'No paths in content' => [ + 'content without paths', + [] + ], + 'Relevant rendition path in content' => [ + 'content {{media url=".renditions/testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant wysiwyg rendition path in content' => [ + 'content <img src="https://domain.com/media/.renditions/testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant rendition path content with pub' => [ + '/pub/media/.renditions/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Relevant rendition path content' => [ + '/media/.renditions/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Relevant existing media paths w/o rendition in content' => [ + 'content {{media url="testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant existing paths w/o rendition in content with pub' => [ + '/pub/media/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Non-existing rendition paths in content' => [ + 'content {{media url=".renditions/non-existing-path.png"}} content', + [] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php new file mode 100644 index 0000000000000..9655f3949d404 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\MediaGalleryRenditions\Model\Config; +use PHPUnit\Framework\TestCase; + +class GenerateRenditionsTest extends TestCase +{ + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var Config + */ + private $renditionSizeConfig; + + /** + * @var DriverInterface + */ + private $driver; + + protected function setup(): void + { + $this->generateRenditions = Bootstrap::getObjectManager()->get(GenerateRenditionsInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->renditionSizeConfig = Bootstrap::getObjectManager()->get(Config::class); + } + + public static function tearDownAfterClass(): void + { + /** @var WriteInterface $mediaDirectory */ + $mediaDirectory = Bootstrap::getObjectManager()->get( + Filesystem::class + )->getDirectoryWrite( + DirectoryList::MEDIA + ); + if ($mediaDirectory->isExist($mediaDirectory->getAbsolutePath() . '/.renditions')) { + $mediaDirectory->delete($mediaDirectory->getAbsolutePath() . '/.renditions'); + } + } + + /** + * @dataProvider renditionsImageProvider + * + * Test for generation of rendition images. + * + * @param string $path + * @param string $renditionPath + * @throws LocalizedException + */ + public function testExecute(string $path, string $renditionPath): void + { + $this->copyImage($path); + $this->generateRenditions->execute([$path]); + $expectedRenditionPath = $this->mediaDirectory->getAbsolutePath($renditionPath); + list($imageWidth, $imageHeight) = getimagesize($expectedRenditionPath); + $this->assertFileExists($expectedRenditionPath); + $this->assertLessThanOrEqual( + $this->renditionSizeConfig->getWidth(), + $imageWidth, + 'Generated renditions image width should be less than or equal to configured value' + ); + $this->assertLessThanOrEqual( + $this->renditionSizeConfig->getHeight(), + $imageHeight, + 'Generated renditions image height should be less than or equal to configured value' + ); + } + + /** + * @param array $paths + * @throws FileSystemException + */ + private function copyImage(string $path): void + { + $imagePath = realpath(__DIR__ . '/../../_files' . $path); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($path); + $this->driver->copy( + $imagePath, + $modifiableFilePath + ); + } + + /** + * @return array + */ + public function renditionsImageProvider(): array + { + return [ + 'rendition_image_not_generated' => [ + 'paths' => '/magento_medium_image.jpg', + 'renditionPath' => ".renditions/magento_medium_image.jpg" + ], + 'rendition_image_generated' => [ + 'paths' => '/magento_large_image.jpg', + 'renditionPath' => ".renditions/magento_large_image.jpg" + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php new file mode 100644 index 0000000000000..0f8b61147986c --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class GetRenditionPathTest extends TestCase +{ + + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var DriverInterface + */ + private $driver; + + protected function setup(): void + { + $this->getRenditionPath = Bootstrap::getObjectManager()->get(GetRenditionPathInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + } + + /** + * @dataProvider getImageProvider + * + * Test for getting a rendition path. + */ + public function testExecute(string $path, string $expectedRenditionPath): void + { + $imagePath = realpath(__DIR__ . '/../../_files' . $path); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($path); + $this->driver->copy( + $imagePath, + $modifiableFilePath + ); + $this->assertEquals($expectedRenditionPath, $this->getRenditionPath->execute($path)); + } + + /** + * @return array + */ + public function getImageProvider(): array + { + return [ + 'return_original_path' => [ + 'path' => '/magento_medium_image.jpg', + 'expectedRenditionPath' => '.renditions/magento_medium_image.jpg' + ], + 'return_rendition_path' => [ + 'path' => '/magento_large_image.jpg', + 'expectedRenditionPath' => '.renditions/magento_large_image.jpg' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg new file mode 100644 index 0000000000000..c377daf8fb0b3 Binary files /dev/null and b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg differ diff --git a/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg new file mode 100644 index 0000000000000..6dc8cd69e41c1 Binary files /dev/null and b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg differ diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json new file mode 100644 index 0000000000000..873e0b4a8c60b --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-media-gallery-renditions", + "description": "Magento module that implements height and width fields for for media gallery items.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-renditions-api": "*", + "magento/module-media-gallery-api": "*", + "magento/framework-message-queue": "*", + "magento/module-cms": "*" + }, + "suggest": { + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditions\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..64f338d53a283 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml @@ -0,0 +1,24 @@ +<?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="system"> + <group id="media_gallery_renditions" translate="label" type="text" sortOrder="1010" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Media Gallery Renditions</label> + <field id="width" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Max Width</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + <field id="height" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Max Height</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/communication.xml b/app/code/Magento/MediaGalleryRenditions/etc/communication.xml new file mode 100644 index 0000000000000..2c343c4f8086a --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/communication.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:Communication/etc/communication.xsd"> + <topic name="media.gallery.renditions.update" is_synchronous="false" request="string[]"> + <handler name="media.gallery.renditions.update.handler" + type="Magento\MediaGalleryRenditions\Model\Queue\UpdateRenditions" method="execute"/> + </topic> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml new file mode 100644 index 0000000000000..58c5aa1f11fd2 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/config.xml @@ -0,0 +1,17 @@ +<?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> + <system> + <media_gallery_renditions> + <width>1000</width> + <height>1000</height> + </media_gallery_renditions> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/di.xml b/app/code/Magento/MediaGalleryRenditions/etc/di.xml new file mode 100644 index 0000000000000..af53810b7f69e --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/di.xml @@ -0,0 +1,31 @@ +<?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"> + <preference for="Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface" type="Magento\MediaGalleryRenditions\Model\GenerateRenditions"/> + <preference for="Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface" type="Magento\MediaGalleryRenditions\Model\GetRenditionPath"/> + <type name="Magento\Cms\Model\Wysiwyg\Images\GetInsertImageContent"> + <plugin name="set_rendition_path" type="Magento\MediaGalleryRenditions\Plugin\SetRenditionPath"/> + </type> + <type name="Magento\MediaGalleryRenditions\Model\Queue\FetchRenditionPathsBatches"> + <arguments> + <argument name="batchSize" xsi:type="number">100</argument> + <argument name="fileExtensions" xsi:type="array"> + <item name="jpg" xsi:type="string">jpg</item> + <item name="jpeg" xsi:type="string">jpeg</item> + <item name="gif" xsi:type="string">gif</item> + <item name="png" xsi:type="string">png</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\Config\Value"> + <plugin name="admin_system_config_media_gallery_renditions" type="Magento\MediaGalleryRenditions\Plugin\UpdateRenditionsOnConfigChange"/> + </type> + <type name="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface"> + <plugin name="delete_renditions_on_assets_delete" type="Magento\MediaGalleryRenditions\Plugin\RemoveRenditions"/> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml new file mode 100644 index 0000000000000..a1fbe5cba558e --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/media_content.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_MediaContentApi:etc/media_content.xsd"> + <search> + <patterns> + <pattern name="media_gallery_renditions">/{{media url=(?:"|&quot;)(?:.renditions)?(.*?)(?:"|&quot;)}}/</pattern> + <pattern name="media_gallery">/{{media url="?((?!.*.renditions).*?)"?}}/</pattern> + <pattern name="wysiwyg">/src=".*\/media\/(?:.renditions\/)*(.*?)"/</pattern> + <pattern name="catalog_image">/^\/?media\/(?:.renditions\/)?(.*)/</pattern> + <pattern name="catalog_image_with_pub">/^\/pub\/?media\/(?:.renditions\/)?(.*)/</pattern> + </patterns> + </search> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/module.xml b/app/code/Magento/MediaGalleryRenditions/etc/module.xml new file mode 100644 index 0000000000000..93bc9f1c214e6 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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_MediaGalleryRenditions"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml new file mode 100644 index 0000000000000..0c584ac12f898 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="media.gallery.renditions.update" queue="media.gallery.renditions.update" + connection="db" handler="Magento\MediaGalleryRenditions\Model\Queue\UpdateRenditions::execute"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml new file mode 100644 index 0000000000000..9618329895230 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd"> + <publisher topic="media.gallery.renditions.update"> + <connection name="db" exchange="magento-db" disabled="false" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml new file mode 100644 index 0000000000000..260e9f5f7f371 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.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-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="MediaGalleryRenditions" topic="media.gallery.renditions.update" + destinationType="queue" destination="media.gallery.renditions.update"/> + </exchange> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/registration.php b/app/code/Magento/MediaGalleryRenditions/registration.php new file mode 100644 index 0000000000000..275c06f752a63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditions', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php b/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php new file mode 100644 index 0000000000000..b3ad5543c17fa --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditionsApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Generate optimized version of media assets based on configuration for insertion to content + */ +interface GenerateRenditionsInterface +{ + /** + * Generate image renditions + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php b/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php new file mode 100644 index 0000000000000..b00c3615d9a29 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditionsApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Based on media assset path provides path to an optimized image version for insertion to the content + */ +interface GetRenditionPathInterface +{ + /** + * Get Renditions image path + * + * @param string $path + * @return string + * @throws LocalizedException + */ + public function execute(string $path): string; +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/README.md b/app/code/Magento/MediaGalleryRenditionsApi/README.md new file mode 100644 index 0000000000000..42478c0c9b520 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditionsApi module + +The Magento_MediaGalleryRenditionsApi module is responsible for the API implementation of Media Gallery Renditions. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditionsApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditionsApi/composer.json b/app/code/Magento/MediaGalleryRenditionsApi/composer.json new file mode 100644 index 0000000000000..6e3c559f001c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-renditions-api", + "description": "Magento module that is responsible for the API implementation of Media Gallery Renditions.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditionsApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml new file mode 100644 index 0000000000000..f3a3f87b61105 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/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_MediaGalleryRenditionsApi"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditionsApi/registration.php b/app/code/Magento/MediaGalleryRenditionsApi/registration.php new file mode 100644 index 0000000000000..bf057f2d2adbf --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditionsApi', + __DIR__ +); diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php index 79c0c9a1a803b..b796d4225d08c 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php @@ -7,6 +7,8 @@ namespace Magento\MediaGallerySynchronization\Model; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; /** @@ -19,19 +21,35 @@ class Consume */ private $synchronize; + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + /** * @param SynchronizeInterface $synchronize + * @param SynchronizeFilesInterface $synchronizeFiles */ - public function __construct(SynchronizeInterface $synchronize) - { + public function __construct( + SynchronizeInterface $synchronize, + SynchronizeFilesInterface $synchronizeFiles + ) { $this->synchronize = $synchronize; + $this->synchronizeFiles = $synchronizeFiles; } /** * Run media files synchronization. + * + * @param array $paths + * @throws LocalizedException */ - public function execute() : void + public function execute(array $paths) : void { - $this->synchronize->execute(); + if (!empty($paths)) { + $this->synchronizeFiles->execute($paths); + } else { + $this->synchronize->execute(); + } } } diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php index 386798d68d9df..ec314416e36ee 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php @@ -33,13 +33,15 @@ public function __construct(PublisherInterface $publisher) } /** - * Publish media content synchronization message to the message queue. + * Publish media content synchronization message to the message queue + * + * @param array $paths */ - public function execute() : void + public function execute(array $paths = []) : void { $this->publisher->publish( self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION, - [self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION] + $paths ); } } diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php new file mode 100644 index 0000000000000..d797acedda6ec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Block\Adminhtml; + +use Magento\Backend\Block\Template; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Image details block + * + * @api + */ +class ImageDetails extends Template +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var Json + */ + private $json; + + /** + * @param Template\Context $context + * @param AuthorizationInterface $authorization + * @param Json $json + * @param array $data + * @param JsonHelper|null $jsonHelper + * @param DirectoryHelper|null $directoryHelper + */ + public function __construct( + Template\Context $context, + AuthorizationInterface $authorization, + Json $json, + array $data = [], + ?JsonHelper $jsonHelper = null, + ?DirectoryHelper $directoryHelper = null + ) { + $this->authorization = $authorization; + $this->json = $json; + parent::__construct($context, $data, $jsonHelper, $directoryHelper); + } + + /** + * Retrieve actions json + * + * @return string + */ + public function getActionsJson(): string + { + $actions = [ + [ + 'title' => __('Cancel'), + 'handler' => 'closeModal', + 'name' => 'cancel', + 'classes' => 'action-default scalable cancel action-quaternary' + ] + ]; + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) { + $actions[] = [ + 'title' => __('Delete Image'), + 'handler' => 'deleteImageAction', + 'name' => 'delete', + 'classes' => 'action-default scalable delete action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { + $actions[] = [ + 'title' => __('Edit Details'), + 'handler' => 'editImageAction', + 'name' => 'edit', + 'classes' => 'action-default scalable edit action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::insert_assets')) { + $actions[] = [ + 'title' => __('Add Image'), + 'handler' => 'addImage', + 'name' => 'add-image', + 'classes' => 'scalable action-primary add-image-action' + ]; + } + + return $this->json->serialize($actions); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php new file mode 100644 index 0000000000000..7e73b1682f79a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Block\Adminhtml; + +use Magento\Backend\Block\Template; +use Magento\Directory\Helper\Data as DirectoryHelperData; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Json\Helper\Data as JsonHelperData; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Image details block + * + * @api + */ +class ImageDetailsStandalone extends Template +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var Json + */ + private $json; + + /** + * @param Template\Context $context + * @param AuthorizationInterface $authorization + * @param Json $json + * @param array $data + * @param JsonHelperData|null $jsonHelper + * @param DirectoryHelperData|null $directoryHelper + */ + public function __construct( + Template\Context $context, + AuthorizationInterface $authorization, + Json $json, + array $data = [], + ?JsonHelperData $jsonHelper = null, + ?DirectoryHelperData $directoryHelper = null + ) { + $this->authorization = $authorization; + $this->json = $json; + parent::__construct($context, $data, $jsonHelper, $directoryHelper); + } + + /** + * Retrieve actions json + * + * @return string + */ + public function getActionsJson(): string + { + $standaloneActions = [ + [ + 'title' => __('Cancel'), + 'handler' => 'closeModal', + 'name' => 'cancel', + 'classes' => 'action-default scalable cancel action-quaternary' + ] + ]; + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) { + $standaloneActions[] = [ + 'title' => __('Delete Image'), + 'handler' => 'deleteImageAction', + 'name' => 'delete', + 'classes' => 'action-default scalable delete action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { + $standaloneActions[] = [ + 'title' => __('Edit Details'), + 'handler' => 'editImageAction', + 'name' => 'edit', + 'classes' => 'action-default scalable edit action-quaternary' + ]; + } + + return $this->json->serialize($standaloneActions); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php index 3d4af88e4ad67..76c00927b33e0 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -29,7 +29,7 @@ class Create extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::create_folder'; /** * @var CreateDirectoriesByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php index 56f12c5139d65..3dc43e5276860 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -30,7 +30,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_folder'; /** * @var DeleteAssetsByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php index a5d1cee7abf41..2f7766c590033 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -31,7 +31,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_assets'; /** * @var DeleteImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php index f41c489607b15..87a2e7345c407 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php @@ -32,7 +32,7 @@ class SaveDetails extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::edit_assets'; /** * @var UpdateAsset diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php index e965d94b33f0c..4492595bbe6ee 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -28,7 +28,7 @@ class Upload extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::upload_assets'; /** * @var UploadImage diff --git a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php new file mode 100644 index 0000000000000..e72017e20a7f6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; + +/** + * Patch is mechanism, that allows to do atomic upgrade data changes + */ +class AddMediaGalleryPermissions implements + DataPatchInterface, + PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface $moduleDataSetup + */ + private $moduleDataSetup; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup) + { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * Add child resources permissions for user roles with Magento_Cms::media_gallery permission + */ + public function apply(): void + { + $tableName = $this->moduleDataSetup->getTable('authorization_rule'); + $connection = $this->moduleDataSetup->getConnection(); + + if (!$tableName) { + return; + } + + $select = $connection->select() + ->from($tableName, ['role_id']) + ->where('resource_id = "Magento_Cms::media_gallery"'); + + $insertData = $this->getInsertData($connection->fetchCol($select)); + + if (!empty($insertData)) { + $connection->insertMultiple($tableName, $insertData); + } + } + + /** + * Retrieve data to insert to authorization_rule table based on role ids + * + * @param array $roleIds + * @return array + */ + private function getInsertData(array $roleIds): array + { + $newResources = [ + 'Magento_MediaGalleryUiApi::insert_assets', + 'Magento_MediaGalleryUiApi::upload_assets', + 'Magento_MediaGalleryUiApi::edit_assets', + 'Magento_MediaGalleryUiApi::delete_assets', + 'Magento_MediaGalleryUiApi::create_folder', + 'Magento_MediaGalleryUiApi::delete_folder' + ]; + + $data = []; + + foreach ($roleIds as $roleId) { + foreach ($newResources as $resourceId) { + $data[] = [ + 'role_id' => $roleId, + 'resource_id' => $resourceId, + 'permission' => 'allow' + ]; + } + } + + return $data; + } + + /** + * @inheritdoc + */ + public function getAliases(): array + { + return []; + } + + /** + * @inheritdoc + */ + public static function getDependencies(): array + { + return []; + } + + /** + * @inheritdoc + */ + public static function getVersion(): string + { + return '2.4.2'; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml new file mode 100644 index 0000000000000..af2b383143f62 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.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="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup"> + <annotations> + <description>Validates that the provided elemen present on page but have attribute disabled.</description> + </annotations> + <arguments> + <argument name="buttonName" type="string"/> + </arguments> + + <grabMultiple selector="{{AdminEnhancedMediaGalleryActionsSection.notDisabledButtons}}" stepKey="verifyDisabledAttribute"/> + + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">verifyDisabledAttribute</actualResult> + <expectedResult type="array">[{{buttonName}}]</expectedResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml new file mode 100644 index 0000000000000..c212092b657fd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.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="AdminAssertMediaGalleryEmptyActionGroup"> + <annotations> + <description>Requires select folder in directory tree. Assert that selected folder is empty.</description> + </annotations> + + <seeElement selector="{{AdminMediaGalleryGridSection.noDataMessage}}" stepKey="assertNoDataMessageDisplayed" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml new file mode 100644 index 0000000000000..db9d1853df583 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.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="AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup"> + <annotations> + <description>Expand media gallery tmp folder tree</description> + </annotations> + <waitForLoadingMaskToDisappear stepKey="waitLoadingMask"/> + <conditionalClick selector="//li[@id='catalog']/ins" dependentSelector="//li[@id='catalog']/ul" visible="false" stepKey="expandCatalog"/> + <wait time="2" stepKey="waitCatalogExpanded"/> + <conditionalClick selector="//li[@id='catalog/tmp']/ins" dependentSelector="//li[@id='catalog/tmp']/ul" visible="false" stepKey="expandTmp"/> + <wait time="2" stepKey="waitTmpExpanded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml new file mode 100644 index 0000000000000..49aa45426152c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.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="AdminMediaGalleryFolderSelectByFullPathActionGroup"> + <arguments> + <argument name="path" type="string"/> + </arguments> + <wait time="2" stepKey="waitBeforeClickOnFolder"/> + <click selector="//li[@id='{{path}}']" stepKey="selectSubFolder" after="waitBeforeClickOnFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml index 451ef81f0ff9f..53781a65e4898 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml @@ -18,11 +18,11 @@ <argument name="thirdImageFile" type="string"/> </arguments> - <grabAttributeFrom selector="{{AdminEnhancedMediaGalleryGridImagePositionSection.nthImageInGrid('0')}}" userInput="src" + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('0')}}" userInput="src" stepKey="getFirstImageSrcAfterSort"/> - <grabAttributeFrom selector="{{AdminEnhancedMediaGalleryGridImagePositionSection.nthImageInGrid('1')}}" userInput="src" + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('1')}}" userInput="src" stepKey="getSecondImageSrcAfterSort"/> - <grabAttributeFrom selector="{{AdminEnhancedMediaGalleryGridImagePositionSection.nthImageInGrid('2')}}" userInput="src" + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('2')}}" userInput="src" stepKey="getThirdImageSrcAfterSort"/> <assertStringContainsString stepKey="assertFirstImagePositionAfterSort"> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml index 7f9a5aefdf69c..907f2c3116800 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml @@ -12,6 +12,7 @@ <element name="deleteViewButton" type="button" selector="//div[@data-bind='afterRender: \$data.setToolbarNode']//input/following-sibling::div/button[@class='action-delete']"/> <element name="upload" type="input" selector="#image-uploader-input"/> <element name="cancel" type="button" selector="[data-ui-id='cancel-button']"/> + <element name="notDisabledButtons" type="button" selector="//div[@class='page-actions floating-header']/button[not(@disabled='disabled') and not(@id='cancel')]"/> <element name="createFolder" type="button" selector="[data-ui-id='create-folder-button']"/> <element name="deleteFolder" type="button" selector="[data-ui-id='delete-folder-button']"/> <element name="imageSrc" type="text" selector="//div[@class='masonry-image-column' and contains(@data-repeat-index, '0')]//img[contains(@src,'{{src}}')]" parameterized="true"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml similarity index 77% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml rename to app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml index 943f29d5fa851..f35a32b6d3a37 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml @@ -7,7 +7,8 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="AdminEnhancedMediaGalleryGridImagePositionSection"> + <section name="AdminMediaGalleryGridSection"> + <element name="noDataMessage" type="text" selector="div.no-data-message-container"/> <element name="nthImageInGrid" type="text" selector="div[class='masonry-image-column'][data-repeat-index='{{row}}'] img" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml new file mode 100644 index 0000000000000..9738ddedc3cc3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml @@ -0,0 +1,73 @@ +<?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="AdminMediaGalleryCreateFolderAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery cretae folder functionality"/> + <description value="User manages ACL rules for Media Gallery cretae folder functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Create folder"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Create Folder"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml new file mode 100644 index 0000000000000..1d51caf0fc400 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDeleteAssetsAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery delete assets functionality"/> + <description value="User manages ACL rules for Media Gallery delete assets functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete assets"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Delete Images..."/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml new file mode 100644 index 0000000000000..121ad25c93f0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDeleteFolderAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery delete folder functionality"/> + <description value="User manages ACL rules for Media Gallery delete folder functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete folder"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Delete Folder"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml index 963a0b954e45b..5926b115afccf 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryDisabledContentFilterTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> <features value="MediaGallery"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> <title value="User filter asset by disabled content"/> @@ -58,8 +61,8 @@ <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> <argument name="imageName" value="{{ImageMetadata.title}}"/> </actionGroup> - <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clickDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> - + </test> </tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml new file mode 100644 index 0000000000000..c8f8655d11edb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryUploadAssetsAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery upload assets functionality"/> + <description value="User manages ACL rules for Media Gallery upload assets functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Upload assets"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Upload Image"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php new file mode 100644 index 0000000000000..039a1006c79e5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Create Folder button + */ +class CreateFolder implements ButtonProviderInterface +{ + private const ACL_CREATE_FOLDER = 'Magento_MediaGalleryUiApi::create_folder'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Create Folder'), + 'on_click' => 'jQuery("#create_folder").trigger("create_folder");', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 10, + ]; + + if (!$this->authorization->isAllowed(self::ACL_CREATE_FOLDER)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php new file mode 100644 index 0000000000000..10604d65f768f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Delete images button + */ +class DeleteAssets implements ButtonProviderInterface +{ + private const ACL_DELETE_ASSETS= 'Magento_MediaGalleryUiApi::delete_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Delete Images...'), + 'on_click' => 'jQuery(window).trigger("massAction.MediaGallery")', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 50, + ]; + + if (!$this->authorization->isAllowed(self::ACL_DELETE_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php new file mode 100644 index 0000000000000..cb803c1c663e0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Delete Folder button + */ +class DeleteFolder implements ButtonProviderInterface +{ + private const ACL_DELETE_FOLDER = 'Magento_MediaGalleryUiApi::delete_folder'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Delete Folder'), + 'disabled' => 'disabled', + 'on_click' => 'jQuery("#delete_folder").trigger("delete_folder");', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 30, + ]; + if (!$this->authorization->isAllowed(self::ACL_DELETE_FOLDER)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php new file mode 100644 index 0000000000000..6854b79ba2c36 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Add selected button + */ +class InsertAsstes implements ButtonProviderInterface +{ + private const ACL_INSERT_ASSETS = 'Magento_MediaGalleryUiApi::insert_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Add Selected'), + 'on_click' => 'return false;', + 'class' => 'action-primary no-display media-gallery-add-selected', + 'sort_order' => 110, + ]; + + if (!$this->authorization->isAllowed(self::ACL_INSERT_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php new file mode 100644 index 0000000000000..32bbdba88a599 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Upload Image button + */ +class UploadAssets implements ButtonProviderInterface +{ + private const ACL_UPLOAD_ASSETS= 'Magento_MediaGalleryUiApi::upload_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Upload Image'), + 'on_click' => 'jQuery("#image-uploader-input").click();', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 20, + ]; + + if (!$this->authorization->isAllowed(self::ACL_UPLOAD_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php index 269bc1f8bcba7..0ad5ad43f6157 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -10,33 +10,46 @@ use Magento\Framework\UrlInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Container; +use Magento\Framework\AuthorizationInterface; /** * Directories tree component */ class DirectoryTree extends Container { + private const ACL_IMAGE_ACTIONS = [ + 'delete_folder' => 'Magento_MediaGalleryUiApi::delete_folder' + ]; + /** * @var UrlInterface */ private $url; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * Constructor * * @param ContextInterface $context * @param UrlInterface $url + * @param AuthorizationInterface $authorization * @param array $components * @param array $data */ public function __construct( ContextInterface $context, UrlInterface $url, + AuthorizationInterface $authorization, array $components = [], array $data = [] ) { parent::__construct($context, $components, $data); $this->url = $url; + $this->authorization = $authorization; } /** @@ -50,6 +63,7 @@ public function prepare(): void array_replace_recursive( (array) $this->getData('config'), [ + 'allowedActions' => $this->getAllowedActions(), 'getDirectoryTreeUrl' => $this->url->getUrl('media_gallery/directories/gettree'), 'deleteDirectoryUrl' => $this->url->getUrl('media_gallery/directories/delete'), 'createDirectoryUrl' => $this->url->getUrl('media_gallery/directories/create') @@ -57,4 +71,19 @@ public function prepare(): void ) ); } + + /** + * Return allowed actions for media gallery + */ + private function getAllowedActions(): array + { + $allowedActions = []; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } + } + + return $allowedActions; + } } diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php index 481f8ab861f0f..0d48a0d0ff0e1 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -15,12 +15,20 @@ use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\AuthorizationInterface; /** * Overlay column */ class Url extends Column { + private const ACL_IMAGE_ACTIONS = [ + 'image-details' => 'Magento_Cms::media_gallery', + 'insert' => 'Magento_MediaGalleryUiApi::insert_assets', + 'delete' => 'Magento_MediaGalleryUiApi::delete_assets', + 'edit' => 'Magento_MediaGalleryUiApi::edit_assets' + ]; + /** * @var StoreManagerInterface */ @@ -41,6 +49,11 @@ class Url extends Column */ private $storage; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory @@ -48,6 +61,7 @@ class Url extends Column * @param UrlInterface $urlInterface * @param Images $images * @param Storage $storage + * @param AuthorizationInterface $authorization * @param array $components * @param array $data */ @@ -58,6 +72,7 @@ public function __construct( UrlInterface $urlInterface, Images $images, Storage $storage, + AuthorizationInterface $authorization, array $components = [], array $data = [] ) { @@ -66,6 +81,7 @@ public function __construct( $this->urlInterface = $urlInterface; $this->images = $images; $this->storage = $storage; + $this->authorization = $authorization; } /** @@ -98,6 +114,7 @@ public function prepare(): void array_replace_recursive( (array)$this->getData('config'), [ + 'allowedActions' => $this->getAllowedActions(), 'onInsertUrl' => $this->urlInterface->getUrl('cms/wysiwyg_images/oninsert'), 'storeId' => $this->storeManager->getStore()->getId() ] @@ -105,6 +122,21 @@ public function prepare(): void ); } + /** + * Return allowed actions for media gallery image + */ + private function getAllowedActions(): array + { + $allowedActions = []; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } + } + + return $allowedActions; + } + /** * Get URL for the provided media asset path * diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php new file mode 100644 index 0000000000000..7d7b67125df96 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Massactions; + +use Magento\Ui\Component\Container; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Massaction comntainer + */ +class Massaction extends Container +{ + private const ACL_IMAGE_ACTIONS = [ + 'delete_assets' => 'Magento_MediaGalleryUiApi::delete_assets' + ]; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor + * + * @param ContextInterface $context + * @param AuthorizationInterface $authorization + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + AuthorizationInterface $authorization, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array)$this->getData('config'), + [ + 'allowedActions' => $this->getAllowedActions() + ] + ) + ); + } + + /** + * Return allowed actions for media gallery + */ + private function getAllowedActions(): array + { + $allowedActions = []; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } + } + + return $allowedActions; + } +} diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json index f4701306eb369..204e0b37c3bf8 100644 --- a/app/code/Magento/MediaGalleryUi/composer.json +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -12,7 +12,9 @@ "magento/module-media-gallery-metadata-api": "*", "magento/module-media-gallery-synchronization-api": "*", "magento/module-media-content-api": "*", - "magento/module-cms": "*" + "magento/module-cms": "*", + "magento/module-directory": "*", + "magento/module-authorization": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml index f41c0f91b2249..a5eb247bd344f 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -16,7 +16,7 @@ <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> </container> <uiComponent name="media_gallery_listing"/> - <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details.phtml"> + <block name="image.details" class="Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails" template="Magento_MediaGalleryUi::image_details.phtml"> <arguments> <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> </arguments> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml index 7750f22b39ce7..b4f377627c850 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml @@ -10,7 +10,7 @@ <body> <referenceContainer htmlTag="div" htmlClass="media-gallery-container" name="content"> <uiComponent name="standalone_media_gallery_listing"/> - <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> + <block name="image.details" class="Magento\MediaGalleryUi\Block\Adminhtml\ImageDetailsStandalone" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> <arguments> <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> </arguments> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml index a6da20a255192..5df5c1a6c4cbd 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -use Magento\Backend\Block\Template; +use Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails; use Magento\Framework\Escaper; // phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength -/** @var Template $block */ +/** @var ImageDetails $block */ /** @var Escaper $escaper */ ?> @@ -73,37 +73,10 @@ use Magento\Framework\Escaper; "modalWindowSelector": ".media-gallery-image-details", "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", - "actionsList": [ - { - "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", - "handler": "closeModal", - "name": "cancel", - "classes": "action-default scalable cancel action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", - "handler": "deleteImageAction", - "name": "delete", - "classes": "action-default scalable delete action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", - "handler": "editImageAction", - "name": "edit", - "classes": "action-default scalable edit action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Add Image')); ?>", - "handler": "addImage", - "name": "add-image", - "classes": "scalable action-primary add-image-action" - } - ] + "actionsList": <?= /* @noEscape */ $block->getActionsJson() ?> } } } } } </script> - - diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml index 288a2eaee5221..fdae0a549606c 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -4,10 +4,8 @@ * See COPYING.txt for license details. */ -use Magento\Backend\Block\Template; - // phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength -/** @var Template $block */ +/** @var \Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails $block */ /** @var \Magento\Framework\Escaper $escaper */ ?> @@ -71,31 +69,10 @@ use Magento\Backend\Block\Template; "modalWindowSelector": ".media-gallery-image-details", "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", - "actionsList": [ - { - "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", - "handler": "closeModal", - "name": "cancel", - "classes": "action-default scalable cancel action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", - "handler": "deleteImageAction", - "name": "delete", - "classes": "action-default scalable delete action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", - "handler": "editImageAction", - "name": "edit", - "classes": "action-default scalable edit action-quaternary" - } - ] + "actionsList": <?= /* @noEscape */ $block->getActionsJson() ?> } } } } } </script> - - diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml index 66731b1cbae6f..b7307f9a74fae 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -16,43 +16,17 @@ </argument> <settings> <buttons> - <button name="add_selected"> - <param name="on_click" xsi:type="string">return false;</param> - <param name="sort_order" xsi:type="number">110</param> - <class>action-primary no-display media-gallery-add-selected</class> - <label translate="true">Add Selected</label> - </button> + <button name="add_selected" class="Magento\MediaGalleryUi\Ui\Component\Control\InsertAsstes"/> <button name="cancel"> <param name="on_click" xsi:type="string">MediabrowserUtility.closeDialog();</param> <param name="sort_order" xsi:type="number">1</param> <class>cancel action-quaternary</class> <label translate="true">Cancel</label> </button> - <button name="upload_image"> - <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> - <class>action-add scalable media-gallery-actions-buttons</class> - <param name="sort_order" xsi:type="number">20</param> - <label translate="true">Upload Image</label> - </button> - <button name="delete_folder"> - <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> - <param name="disabled" xsi:type="string">disabled</param> - <param name="sort_order" xsi:type="number">30</param> - <class>action-default scalable media-gallery-actions-buttons</class> - <label translate="true">Delete Folder</label> - </button> - <button name="create_folder"> - <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> - <param name="sort_order" xsi:type="number">10</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Create Folder</label> - </button> - <button name="delete_massaction"> - <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> - <param name="sort_order" xsi:type="number">50</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Images...</label> - </button> + <button name="upload_image" class="Magento\MediaGalleryUi\Ui\Component\Control\UploadAssets"/> + <button name="delete_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteFolder"/> + <button name="create_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\CreateFolder"/> + <button name="delete_massaction" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteAssets"/> </buttons> <spinner>media_gallery_columns</spinner> <deps> @@ -207,6 +181,7 @@ <container name="media_gallery_massactions" displayArea="sorting" sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Massactions\Massaction" component="Magento_MediaGalleryUi/js/grid/massaction/massactions" template="Magento_MediaGalleryUi/grid/massactions/count" > <argument name="data" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml index 3656a8ea25f74..a53a46c61f75d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -20,30 +20,10 @@ <dep>standalone_media_gallery_listing.media_gallery_listing_data_source</dep> </deps> <buttons> - <button name="delete_folder"> - <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> - <param name="disabled" xsi:type="string">disabled</param> - <param name="sort_order" xsi:type="number">20</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Folder</label> - </button> - <button name="create_folder"> - <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> - <param name="sort_order" xsi:type="number">30</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Create Folder</label> - </button> - <button name="delete_massaction"> - <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> - <param name="sort_order" xsi:type="number">50</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Images...</label> - </button> - <button name="upload_image"> - <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Upload Image</label> - </button> + <button name="upload_image" class="Magento\MediaGalleryUi\Ui\Component\Control\UploadAssets"/> + <button name="delete_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteFolder"/> + <button name="create_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\CreateFolder"/> + <button name="delete_massaction" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteAssets"/> </buttons> </settings> <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> @@ -194,6 +174,7 @@ <container name="media_gallery_massactions" displayArea="sorting" sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Massactions\Massaction" component="Magento_MediaGalleryUi/js/grid/massaction/massactions" template="Magento_MediaGalleryUi/grid/massactions/count" > <argument name="data" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less index 6b3cd610f0348..4b0d8f7dec89e 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -99,6 +99,9 @@ .media-gallery-container { + .action-disabled { + opacity: .5; + } .masonry-image-grid .no-data-message-container, .masonry-image-grid .error-message-container { left: 50%; diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js index 6d8d38a1ca1d6..5555baeabb66a 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -19,6 +19,7 @@ define([ return Component.extend({ defaults: { + allowedActions: [], directoryTreeSelector: '#media-gallery-directory-tree', deleteButtonSelector: '#delete_folder', createFolderButtonSelector: '#create_folder', @@ -186,6 +187,10 @@ define([ * @param {String} folderId */ setActive: function (folderId) { + if (!this.allowedActions.includes('delete_folder')) { + return; + } + this.selectedFolder(folderId); $(this.deleteButtonSelector).removeAttr('disabled').removeClass('disabled'); } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js index 2e1e9a980cd59..84f253da826a8 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -17,6 +17,7 @@ define([ return Component.extend({ defaults: { + allowedActions: [], filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', directoryTreeSelector: '#media-gallery-directory-tree', getDirectoryTreeUrl: 'media_gallery/directories/gettree', @@ -32,7 +33,8 @@ define([ }, viewConfig: [{ component: 'Magento_MediaGalleryUi/js/directory/directories', - name: '${ $.name }_directories' + name: '${ $.name }_directories', + allowedActions: '${ $.allowedActions }' }] }, diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js index bf852d0ddae68..974e22e23737c 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -13,10 +13,13 @@ define([ return Column.extend({ defaults: { bodyTmpl: 'Magento_MediaGalleryUi/grid/columns/image', + messageContentSelector: 'ul.messages', + mediaGalleryContainerSelector: '.media-gallery-container', deleteImageUrl: 'media_gallery/image/delete', addSelectedBtnSelector: '#add_selected', deleteSelectedBtnSelector: '#delete_selected', selected: null, + allowedActions: [], fields: { id: 'id', url: 'url', @@ -39,7 +42,8 @@ define([ { component: 'Magento_MediaGalleryUi/js/grid/columns/image/actions', name: '${ $.name }_actions', - imageModelName: '${ $.name }' + imageModelName: '${ $.name }', + allowedActions: '${ $.allowedActions }' } ] }, @@ -222,8 +226,15 @@ define([ toggleAddSelectedButton: function () { if (this.selected() === null) { this.hideAddSelectedAndDeleteButon(); - } else { + + return; + } + + if (this.allowedActions.includes('insert')) { $(this.addSelectedBtnSelector).removeClass('no-display'); + } + + if (this.allowedActions.includes('delete')) { $(this.deleteSelectedBtnSelector).removeClass('no-display'); } }, @@ -270,6 +281,7 @@ define([ */ addMessage: function (code, message) { this.messages().add(code, message); + this.scrollToMessageContent(); this.messages().scheduleCleanup(); }, @@ -284,6 +296,20 @@ define([ !this.massaction().massActionMode()) { this.deselectImage(); } + }, + + /** + * Scroll to the top of media gallery page + */ + scrollToMessageContent: function () { + var scrollTargetElement = $(this.messageContentSelector), + scrollTargetContainer = $(this.mediaGalleryContainerSelector); + + scrollTargetContainer.find(scrollTargetElement).get(0).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest' + }); } }); }); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js index 38743c8d83d3b..76e051072285a 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -8,7 +8,8 @@ define([ 'uiComponent', 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', - 'mage/translate' + 'mage/translate', + 'Magento_Ui/js/lib/view/utils/async' ], function ($, _, Component, deleteImageWithDetailConfirmation, image, $t) { 'use strict'; @@ -17,20 +18,24 @@ define([ template: 'Magento_MediaGalleryUi/grid/columns/image/actions', mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + allowedActions: [], actionsList: [ { name: 'image-details', title: $t('View Details'), + classes: 'action-menu-item', handler: 'viewImageDetails' }, { name: 'edit', title: $t('Edit'), + classes: 'action-menu-item', handler: 'editImageDetails' }, { name: 'delete', title: $t('Delete'), + classes: 'action-menu-item media-gallery-delete-assets', handler: 'deleteImageAction' } ], @@ -50,6 +55,16 @@ define([ this._super(); this.initEvents(); + this.actionsList = this.actionsList.filter(function (item) { + return this.allowedActions.includes(item.name); + }.bind(this)); + + if (!this.allowedActions.includes('delete')) { + $.async('.media-gallery-delete-assets', function () { + $('.media-gallery-delete-assets').unbind('click').addClass('action-disabled'); + }); + } + return this; }, diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js index 03e82e65b5db5..a20239fb1165e 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js @@ -16,6 +16,7 @@ define([ return Component.extend({ defaults: { + allowedActions: [], deleteButtonSelector: '#delete_selected_massaction', deleteImagesSelector: '#delete_massaction', mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', @@ -106,6 +107,10 @@ define([ * If images records less than one, disable "delete images" button */ checkButtonVisibility: function () { + if (!this.allowedActions.includes('delete_assets')) { + return; + } + if (this.imageItems.length < 1) { $(this.deleteImagesSelector).addClass('disabled'); } else { diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html index 042e119b9f40e..72447196cea55 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html @@ -7,9 +7,9 @@ <each args="{ data: actionsList, as: 'action' }"> <li> - <a class="action-menu-item" href="" text="action.title" + <a href="#" text="action.title" click="$parent[action.handler].bind($parent, $row())" - attr="{'data-action': 'item-' + action.name}"> + attr="{'data-action': 'item-' + action.name, class: action.classes}"> </a> </li> -</each> \ No newline at end of file +</each> diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json index f8d5ef11058c1..d577f50523f13 100644 --- a/app/code/Magento/MediaGalleryUiApi/composer.json +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -5,6 +5,9 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*" }, + "suggest": { + "magento/module-cms": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", diff --git a/app/code/Magento/MediaGalleryUiApi/etc/acl.xml b/app/code/Magento/MediaGalleryUiApi/etc/acl.xml new file mode 100644 index 0000000000000..c496c57d51322 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/etc/acl.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:framework:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Backend::content"> + <resource id="Magento_Backend::content_elements"> + <resource id="Magento_Cms::media_gallery" title="Media Gallery" translate="title"> + <resource id="Magento_MediaGalleryUiApi::insert_assets" title="Insert assets into the content" translate="title" sortOrder="40"/> + <resource id="Magento_MediaGalleryUiApi::upload_assets" title="Upload assets" translate="title" sortOrder="50"/> + <resource id="Magento_MediaGalleryUiApi::edit_assets" title="Edit asset details" translate="title" sortOrder="60"/> + <resource id="Magento_MediaGalleryUiApi::delete_assets" title="Delete assets" translate="title" sortOrder="70"/> + <resource id="Magento_MediaGalleryUiApi::create_folder" title="Create folder" translate="title" sortOrder="80"/> + <resource id="Magento_MediaGalleryUiApi::delete_folder" title="Delete folder" translate="title" sortOrder="90"/> + </resource> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php index a1228903e2323..c19dbc2c429ae 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -119,9 +119,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) $groupId = null; if (empty($customerVatNumber) || false == $this->customerVat->isCountryInEU($customerCountryCode)) { - $groupId = $customer->getId() ? $this->groupManagement->getDefaultGroup( - $storeId - )->getId() : $this->groupManagement->getNotLoggedInGroup()->getId(); + $groupId = $customer->getId() ? $quote->getCustomerGroupId() : + $this->groupManagement->getNotLoggedInGroup()->getId(); } else { // Magento always has to emulate group even if customer uses default billing/shipping address $groupId = $this->customerVat->getCustomerGroupIdBasedOnVatNumber( diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index 1920b088b1c0e..ae2a4734215ad 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -13,7 +13,7 @@ use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupManagementInterface; -use Magento\Customer\Helper\Address; +use Magento\Customer\Helper\Address as CustomerAddress; use Magento\Customer\Model\Session; use Magento\Customer\Model\Vat; use Magento\Framework\Event\Observer; @@ -21,10 +21,11 @@ use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\Quote\Api\Data\ShippingInterface; use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; use Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver; use Magento\Quote\Observer\Frontend\Quote\Address\VatValidator; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; /** * Class CollectTotalsTest @@ -124,7 +125,7 @@ protected function setUp(): void true, ['getStoreId', 'getCustomAttribute', 'getId', '__wakeup'] ); - $this->customerAddressMock = $this->createMock(Address::class); + $this->customerAddressMock = $this->createMock(CustomerAddress::class); $this->customerVatMock = $this->createMock(Vat::class); $this->customerDataFactoryMock = $this->getMockBuilder(CustomerInterfaceFactory::class) ->addMethods(['mergeDataObjectWithArray']) @@ -174,6 +175,7 @@ protected function setUp(): void $shippingAssignmentMock = $this->getMockForAbstractClass(ShippingAssignmentInterface::class); $shippingMock = $this->getMockForAbstractClass(ShippingInterface::class); + $shippingAssignmentMock->expects($this->once())->method('getShipping')->willReturn($shippingMock); $shippingMock->expects($this->once())->method('getAddress')->willReturn($this->quoteAddressMock); @@ -185,7 +187,6 @@ protected function setUp(): void $this->quoteMock->expects($this->any()) ->method('getCustomer') ->willReturn($this->customerMock); - $this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class); $this->customerSession = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() @@ -266,26 +267,20 @@ public function testDispatchWithDefaultCustomerGroupId() ->willReturn('customerCountryCode'); $this->quoteAddressMock->expects($this->once())->method('getVatId')->willReturn(null); - $this->quoteMock->expects($this->once()) + $this->quoteMock->expects($this->exactly(2)) ->method('getCustomerGroupId') ->willReturn('customerGroupId'); $this->customerMock->expects($this->once())->method('getId')->willReturn('1'); - $this->groupManagementMock->expects($this->once()) - ->method('getDefaultGroup') - ->willReturn($this->groupInterfaceMock); - $this->groupInterfaceMock->expects($this->once()) - ->method('getId')->willReturn('defaultCustomerGroupId'); + /** Assertions */ $this->quoteAddressMock->expects($this->once()) ->method('setPrevQuoteCustomerGroupId') ->with('customerGroupId'); - $this->quoteMock->expects($this->once())->method('setCustomerGroupId')->with('defaultCustomerGroupId'); $this->customerDataFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->customerMock); $this->quoteMock->expects($this->once())->method('setCustomer')->with($this->customerMock); - /** SUT execution */ $this->model->execute($this->observerMock); } @@ -343,7 +338,7 @@ public function testDispatchWithAddressCustomerVatIdAndCountryId() $customerVat = "123123123"; $defaultShipping = 1; - $customerAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $customerAddress = $this->createMock(Address::class); $customerAddress->expects($this->any()) ->method("getVatId") ->willReturn($customerVat); @@ -379,8 +374,8 @@ public function testDispatchWithEmptyShippingAddress() $customerCountryCode = "DE"; $customerVat = "123123123"; $defaultShipping = 1; - $customerAddress = $this->getMockForAbstractClass(AddressInterface::class); + $customerAddress->expects($this->once()) ->method("getCountryId") ->willReturn($customerCountryCode); diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php index 7ad2e5dde2985..e14d8bde6be74 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -110,6 +110,10 @@ private function findRelations(array $products, array $loadAttributes, int $link //Matching products with related products. $relationsData = []; foreach ($relations as $productId => $relatedIds) { + //Remove related products that not exist in map list. + $relatedIds = array_filter($relatedIds, function ($relatedId) use ($relatedProducts) { + return isset($relatedProducts[$relatedId]); + }); $relationsData[$productId] = array_map( function ($id) use ($relatedProducts) { return $relatedProducts[$id]; diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index 2edd76879d8dc..5f739b2595418 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -27,15 +27,10 @@ protected function _construct() $this->_mode = 'add'; $this->buttonList->update('save', 'label', __('Save Review')); $this->buttonList->update('save', 'id', 'save_button'); + $this->buttonList->update('save', 'style', 'display: none;'); $this->buttonList->update('reset', 'id', 'reset_button'); + $this->buttonList->update('reset', 'style', 'display: none;'); $this->buttonList->update('reset', 'onclick', 'window.review.formReset()'); - $this->_formScripts[] = ' - require(["prototype"], function(){ - toggleParentVis("add_review_form"); - toggleVis("save_button"); - toggleVis("reset_button"); - }); - '; // @codingStandardsIgnoreStart $this->_formInitScripts[] = ' require(["jquery","Magento_Review/js/rating","prototype"], function(jQuery, rating){ diff --git a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php index 04e6343eb43ca..efffa7a02678a 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Review\Block\Adminhtml\Add; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml add product review form * @@ -26,6 +29,11 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic */ protected $_systemStore; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -33,6 +41,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Store\Model\System\Store $systemStore * @param \Magento\Review\Helper\Data $reviewData * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -40,10 +49,12 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Store\Model\System\Store $systemStore, \Magento\Review\Helper\Data $reviewData, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null ) { $this->_reviewData = $reviewData; $this->_systemStore = $systemStore; + $this->secureRenderer = $htmlRenderer ?: ObjectManager::getInstance()->get(SecureHtmlRenderer::class); parent::__construct($context, $registry, $formFactory, $data); } @@ -59,6 +70,8 @@ protected function _prepareForm() $form = $this->_formFactory->create(); $fieldset = $form->addFieldset('add_review_form', ['legend' => __('Review Details')]); + $beforeHtml = $this->secureRenderer->renderStyleAsTag('display: none;', '#edit_form'); + $fieldset->setBeforeElementHtml($beforeHtml); $fieldset->addField('product_name', 'note', ['label' => __('Product'), 'text' => 'product_name']); diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml index 92c01cf380746..4d75589c40e9c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml @@ -28,5 +28,6 @@ <element name="discountAmountColumn" type="text" selector=".order-invoice-tables .col-discount .price"/> <element name="totalColumn" type="text" selector=".order-invoice-tables .col-total .price"/> <element name="updateQty" type="button" selector=".order-invoice-tables tfoot button[data-ui-id='order-items-update-button']"/> + <element name="bundleItem" type="text" selector="#invoice_item_container .option-value"/> </section> </sections> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml index b2e84691a45cf..029bcb8abcc25 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml @@ -10,15 +10,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,7 +27,7 @@ </div> <?php endif; ?> </dd> - <?php else : ?> + <?php else: ?> <dd> <?= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?> </dd> @@ -37,10 +37,10 @@ <?php endif; ?> <?php /* downloadable */ ?> - <?php if ($links = $block->getLinks()) : ?> + <?php if ($links = $block->getLinks()): ?> <dl class="item options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?></dt> - <?php foreach ($links->getPurchasedItems() as $link) : ?> + <?php foreach ($links->getPurchasedItems() as $link): ?> <dd><?= $block->escapeHtml($link->getLinkTitle()) ?></dd> <?php endforeach; ?> </dl> @@ -48,12 +48,14 @@ <?php /* EOF downloadable */ ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) : ?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> @@ -61,7 +63,9 @@ <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> <?= $block->getItemRowTotalHtml() ?> </td> - <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"><?= /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?></td> + <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"> + <?= /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?> + </td> <td class="col total" data-th="<?= $block->escapeHtml(__('Row Total')) ?>"> <?= $block->getItemRowTotalAfterDiscountHtml() ?> </td> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml index 0176582f0fcd7..d9542d13aba6d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml @@ -10,15 +10,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,19 +27,21 @@ </div> <?php endif; ?> </dd> - <?php else : ?> + <?php else: ?> <dd><?= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) :?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml index 51e43476238be..9cae232ca6541 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml @@ -10,15 +10,15 @@ $_item = $block->getItem(); <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,43 +27,46 @@ $_item = $block->getItem(); </div> <?php endif; ?> </dd> - <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <?php else: ?> + <?php $optionValue = isset($_option['print_value']) ? $_option['print_value'] : $_option['value'] ?> + <dd><?= $block->escapeHtml($optionValue) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addtInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addtInfoBlock) : ?> + <?php if ($addtInfoBlock): ?> <?= $addtInfoBlock->setItem($_item)->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"> <ul class="items-qty"> - <?php if ($block->getItem()->getQtyOrdered() > 0) : ?> + <?php if ($block->getItem()->getQtyOrdered() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Ordered')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyOrdered() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyShipped() > 0) : ?> + <?php if ($block->getItem()->getQtyShipped() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Shipped')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyShipped() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyCanceled() > 0) : ?> + <?php if ($block->getItem()->getQtyCanceled() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Canceled')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyCanceled() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyRefunded() > 0) : ?> + <?php if ($block->getItem()->getQtyRefunded() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Refunded')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyRefunded() ?></span> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml index 26fe74b0fc454..6c7567a8cd14b 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml @@ -9,15 +9,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -26,18 +26,21 @@ </div> <?php endif; ?> </dd> - <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <?php else: ?> + <?php $optionValue = isset($_option['print_value']) ? $_option['print_value'] : $_option['value'] ?> + <dd><?= $block->escapeHtml($optionValue) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) : ?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Shipped')) ?>"><?= (int) $_item->getQty() ?></td> </tr> diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php index 53459f2c3e52f..d1440a2b547a4 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php @@ -15,13 +15,14 @@ use Magento\Framework\View\Result\Layout; use Magento\Framework\App\ResponseInterface; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Export Coupons to csv file * * Class \Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCouponsCsv */ -class ExportCouponsCsv extends Quote implements HttpGetActionInterface +class ExportCouponsCsv extends Quote implements HttpGetActionInterface, HttpPostActionInterface { /** * Export coupon codes as CSV file diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php index fa3d4455410c4..401d8aea1aded 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php @@ -15,13 +15,14 @@ use Magento\Framework\View\Result\Layout; use Magento\Framework\App\ResponseInterface; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Export coupons to xml file * * Class \Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCouponsXml */ -class ExportCouponsXml extends Quote implements HttpGetActionInterface +class ExportCouponsXml extends Quote implements HttpGetActionInterface, HttpPostActionInterface { /** * Export coupon codes as excel xml file diff --git a/app/code/Magento/Tax/Pricing/Render/Adjustment.php b/app/code/Magento/Tax/Pricing/Render/Adjustment.php index 8613e62f2983e..0e5c619790a97 100644 --- a/app/code/Magento/Tax/Pricing/Render/Adjustment.php +++ b/app/code/Magento/Tax/Pricing/Render/Adjustment.php @@ -38,6 +38,8 @@ public function __construct( } /** + * Apply the right HTML output to the adjustment + * * @return string */ protected function apply() @@ -173,4 +175,16 @@ public function displayPriceExcludingTax() { return $this->taxHelper->displayPriceExcludingTax(); } + + /** + * Obtain a value for data-price-type attribute + * + * @return string + */ + public function getDataPriceType(): string + { + return $this->amountRender->getPriceType() === 'finalPrice' + ? 'basePrice' + : 'base' . ucfirst($this->amountRender->getPriceType()); + } } diff --git a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml index e87d1c9eb96aa..685893151bc5a 100644 --- a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml +++ b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml @@ -6,12 +6,13 @@ ?> <?php /** @var \Magento\Tax\Pricing\Render\Adjustment $block */ ?> +<?php /** @var $escaper \Magento\Framework\Escaper */ ?> -<?php if ($block->displayBothPrices()) : ?> - <span id="<?= $block->escapeHtmlAttr($block->buildIdWithPrefix('price-excluding-tax-')) ?>" - data-label="<?= $block->escapeHtmlAttr(__('Excl. Tax')) ?>" +<?php if ($block->displayBothPrices()): ?> + <span id="<?= $escaper->escapeHtmlAttr($block->buildIdWithPrefix('price-excluding-tax-')) ?>" + data-label="<?= $escaper->escapeHtmlAttr(__('Excl. Tax')) ?>" data-price-amount="<?= /* @noEscape */ $block->getRawAmount() ?>" - data-price-type="basePrice" + data-price-type="<?= $escaper->escapeHtmlAttr($block->getDataPriceType()); ?>" class="price-wrapper price-excluding-tax"> <span class="price"><?= /* @noEscape */ $block->getDisplayAmountExclTax() ?></span></span> <?php endif; ?> diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml new file mode 100644 index 0000000000000..c7236c33e7cc0 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest"> + <annotations> + <stories value="Reset Error Messages"/> + <title value="Remove Error Message Before Apply Filters"/> + <description value="Test login to Admin UI and Remove Error Message Before Apply Filters"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37450"/> + <group value="ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="defaultSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="rootCategory" /> + </createData> + <createData entity="defaultSimpleProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="rootCategory" /> + </createData> + + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <!-- Create second store --> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + </before> + <after> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteProduct2" createDataKey="createProduct2"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Filter created simple product in grid and add category and website created in create data--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowOfCreatedSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <actionGroup ref="AddWebsiteToProductActionGroup" stepKey="updateSimpleProductAddingWebsiteCreated"> + <argument name="website" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Search updated simple product(from above step) in the grid by StoreView and Name--> + <actionGroup ref="FilterProductInGridByStoreViewAndNameActionGroup" stepKey="searchCreatedSimpleProductInGrid"> + <argument name="storeView" value="{{customStoreEN.name}}"/> + <argument name="productName" value="$$createProduct2.name$$"/> + </actionGroup> + + <!--Go to stores and delete website created in create data--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Go to grid page and verify AssertErrorMessage--> + <actionGroup ref="AssertErrorMessageAfterDeletingWebsiteActionGroup" stepKey="verifyErrorMessage"> + <argument name="errorMessage" value="Something went wrong with processing the default view and we have restored the filter to its original state."/> + </actionGroup> + + <!--Apply new filters to verify error message is removed --> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.storeViewDropdown('Default Store View')}}" stepKey="clickStoreViewDropdown"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="$$createProduct.name$$" stepKey="fillProductNameInNameFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <see selector="{{AdminProductGridFilterSection.nthRow('1')}}" userInput="$$createProduct.name$$" stepKey="seeFirstRowToVerifyProductVisibleInGrid"/> + <dontSeeElement selector="{{AdminMessagesSection.error}}" stepKey="dontSeeErrorMessage"/> + + </test> +</tests> diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js index 9a34e57df86c7..65443fadf8007 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js @@ -668,7 +668,7 @@ define([ * @returns {Object} Chainable */ toggleListVisible: function () { - this.listVisible(!this.listVisible()); + this.listVisible(!this.disabled() && !this.listVisible()); return this; }, diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js index fe33389eabad4..848ad60219a2b 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js @@ -200,6 +200,7 @@ define([ * @returns {Filters} Chainable. */ apply: function () { + $('body').notification('clear'); this.set('applied', removeEmpty(this.filters)); return this; diff --git a/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js index be9044143c5a4..3c5e72d4d66ed 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js @@ -13,10 +13,12 @@ define([ return Component.extend({ defaults: { listingNamespace: null, + bookmarkProvider: 'componentType = bookmark, ns = ${ $.listingNamespace }', filterProvider: 'componentType = filters, ns = ${ $.listingNamespace }', filterKey: 'filters', searchString: location.search, modules: { + bookmarks: '${ $.bookmarkProvider }', filterComponent: '${ $.filterProvider }' } }, @@ -49,6 +51,16 @@ define([ return; } + if (!_.isUndefined(this.bookmarks())) { + if (!_.size(this.bookmarks().getViewData(this.bookmarks().defaultIndex))) { + setTimeout(function () { + this.apply(); + }.bind(this), 500); + + return; + } + } + if (Object.keys(urlFilter).length) { applied = this.filterComponent().get('applied'); filters = $.extend({}, applied, urlFilter); diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 195c3f397ff18..b05b70cfcbc71 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -5,6 +5,16 @@ */ namespace Magento\Widget\Model; +use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\DataObject; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Asset\Repository; +use Magento\Framework\View\Asset\Source; +use Magento\Framework\View\FileSystem; +use Magento\Widget\Helper\Conditions; +use Magento\Widget\Model\Config\Data; + /** * Widget model for different purposes * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -15,32 +25,32 @@ class Widget { /** - * @var \Magento\Widget\Model\Config\Data + * @var Data */ protected $dataStorage; /** - * @var \Magento\Framework\App\Cache\Type\Config + * @var Config */ protected $configCacheType; /** - * @var \Magento\Framework\View\Asset\Repository + * @var Repository */ protected $assetRepo; /** - * @var \Magento\Framework\View\Asset\Source + * @var Source */ protected $assetSource; /** - * @var \Magento\Framework\View\FileSystem + * @var FileSystem */ protected $viewFileSystem; /** - * @var \Magento\Framework\Escaper + * @var Escaper */ protected $escaper; @@ -50,30 +60,35 @@ class Widget protected $widgetsArray = []; /** - * @var \Magento\Widget\Helper\Conditions + * @var Conditions */ protected $conditionsHelper; /** - * @var \Magento\Framework\Math\Random + * @var Random */ private $mathRandom; /** - * @param \Magento\Framework\Escaper $escaper - * @param \Magento\Widget\Model\Config\Data $dataStorage - * @param \Magento\Framework\View\Asset\Repository $assetRepo - * @param \Magento\Framework\View\Asset\Source $assetSource - * @param \Magento\Framework\View\FileSystem $viewFileSystem - * @param \Magento\Widget\Helper\Conditions $conditionsHelper + * @var string[] + */ + private $reservedChars = ['}', '{']; + + /** + * @param Escaper $escaper + * @param Data $dataStorage + * @param Repository $assetRepo + * @param Source $assetSource + * @param FileSystem $viewFileSystem + * @param Conditions $conditionsHelper */ public function __construct( - \Magento\Framework\Escaper $escaper, - \Magento\Widget\Model\Config\Data $dataStorage, - \Magento\Framework\View\Asset\Repository $assetRepo, - \Magento\Framework\View\Asset\Source $assetSource, - \Magento\Framework\View\FileSystem $viewFileSystem, - \Magento\Widget\Helper\Conditions $conditionsHelper + Escaper $escaper, + Data $dataStorage, + Repository $assetRepo, + Source $assetSource, + FileSystem $viewFileSystem, + Conditions $conditionsHelper ) { $this->escaper = $escaper; $this->dataStorage = $dataStorage; @@ -110,14 +125,11 @@ public function getWidgetByClassType($type) $widgets = $this->getWidgets(); /** @var array $widget */ foreach ($widgets as $widget) { - if (isset($widget['@'])) { - if (isset($widget['@']['type'])) { - if ($type === $widget['@']['type']) { - return $widget; - } - } + if (isset($widget['@']['type']) && $type === $widget['@']['type']) { + return $widget; } } + return null; } @@ -131,6 +143,7 @@ public function getWidgetByClassType($type) */ public function getConfigAsXml($type) { + // phpstan:ignore return $this->getXmlElementByType($type); } @@ -296,42 +309,70 @@ public function getWidgetsArray($filters = []) */ public function getWidgetDeclaration($type, $params = [], $asIs = true) { - $directive = '{{widget type="' . $type . '"'; $widget = $this->getConfigAsObject($type); + $params = array_filter($params, function ($value) { + return $value !== null && $value !== ''; + }); + + $directiveParams = ''; foreach ($params as $name => $value) { // Retrieve default option value if pre-configured - if ($name == 'conditions') { - $name = 'conditions_encoded'; - $value = $this->conditionsHelper->encode($value); - } elseif (is_array($value)) { - $value = implode(',', $value); - } elseif (trim($value) == '') { - $parameters = $widget->getParameters(); - if (isset($parameters[$name]) && is_object($parameters[$name])) { - $value = $parameters[$name]->getValue(); - } - } - if (isset($value)) { - $directive .= sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)); - } + $directiveParams .= $this->getDirectiveParam($widget, $name, $value); } - $directive .= $this->getWidgetPageVarName($params); - - $directive .= '}}'; + $directive = sprintf('{{widget type="%s"%s%s}}', $type, $directiveParams, $this->getWidgetPageVarName($params)); if ($asIs) { return $directive; } - $html = sprintf( + return sprintf( '<img id="%s" src="%s" title="%s">', $this->idEncode($directive), $this->getPlaceholderImageUrl($type), $this->escaper->escapeUrl($directive) ); - return $html; + } + + /** + * Returns directive param with prepared value + * + * @param DataObject $widget + * @param string $name + * @param string|array $value + * @return string + */ + private function getDirectiveParam(DataObject $widget, string $name, $value): string + { + if ($name === 'conditions') { + $name = 'conditions_encoded'; + $value = $this->conditionsHelper->encode($value); + } elseif (is_array($value)) { + $value = implode(',', $value); + } elseif (trim($value) === '') { + $parameters = $widget->getParameters(); + if (isset($parameters[$name]) && is_object($parameters[$name])) { + $value = $parameters[$name]->getValue(); + } + } else { + $value = $this->getPreparedValue($value); + } + + return sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)); + } + + /** + * Returns encoded value if it contains reserved chars + * + * @param string $value + * @return string + */ + private function getPreparedValue(string $value): string + { + $pattern = sprintf('/%s/', implode('|', $this->reservedChars)); + + return preg_match($pattern, $value) ? rawurlencode($value) : $value; } /** diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 5d9b1911bc292..7d30d958b5228 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -398,7 +398,11 @@ protected function _renderFiltersBefore() $availableProductTypes = $this->salesConfig->getAvailableProductTypes(); $this->getSelect()->join( ['cat_prod' => $this->getTable('catalog_product_entity')], - $this->getConnection()->quoteInto('cat_prod.type_id IN (?)', $availableProductTypes), + $this->getConnection() + ->quoteInto( + "cat_prod.type_id IN (?) AND {$mainTableName}.product_id = cat_prod.entity_id", + $availableProductTypes + ), [] ); } diff --git a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less index fa158589feb96..654236e143a29 100644 --- a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less +++ b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less @@ -18,15 +18,10 @@ // _____________________________________________ .currency-addon { + .lib-vendor-prefix-display(inline-flex); border: 1px solid rgb(173,173,173); - position: relative; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; flex-flow: row nowrap; + position: relative; width: 100%; .admin__control-text { diff --git a/app/design/frontend/Magento/blank/web/css/source/_navigation.less b/app/design/frontend/Magento/blank/web/css/source/_navigation.less index fad906a089400..f9cca1ca16a18 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_navigation.less +++ b/app/design/frontend/Magento/blank/web/css/source/_navigation.less @@ -28,10 +28,10 @@ .nav-toggle { .lib-icon-font( - @icon-menu, - @_icon-font-size: 28px, - @_icon-font-color: @header-icons-color, - @_icon-font-color-hover: @header-icons-color-hover + @icon-menu, + @_icon-font-size: 28px, + @_icon-font-color: @header-icons-color, + @_icon-font-color-hover: @header-icons-color-hover ); .lib-icon-text-hide(); cursor: pointer; @@ -54,13 +54,13 @@ .parent { .level-top { - position: relative; .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: 42px, - @_icon-font-position: after, - @_icon-font-display: block + @_icon-font-content: @icon-down, + @_icon-font-size: 42px, + @_icon-font-position: after, + @_icon-font-display: block ); + position: relative; &:after { position: absolute; @@ -70,8 +70,8 @@ &.ui-state-active { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } } @@ -82,12 +82,10 @@ -webkit-overflow-scrolling: touch; .lib-css(transition, left .3s, 1); height: 100%; - left: -80%; left: calc(~'-1 * (100% - @{active-nav-indent})'); overflow: auto; position: fixed; top: 0; - width: 80%; width: calc(~'100% - @{active-nav-indent}'); .switcher { @@ -109,13 +107,13 @@ .switcher-trigger { strong { - position: relative; .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: 42px, - @_icon-font-position: after, - @_icon-font-display: block + @_icon-font-content: @icon-down, + @_icon-font-size: 42px, + @_icon-font-position: after, + @_icon-font-display: block ); + position: relative; &:after { position: absolute; @@ -126,16 +124,18 @@ &.active strong { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } } + .switcher-dropdown { .lib-list-reset-styles(); display: none; padding: @indent__s 0; } + .switcher-options { &.active { .switcher-dropdown { @@ -143,6 +143,7 @@ } } } + .header.links { .lib-list-reset-styles(); border-bottom: 1px solid @color-gray82; @@ -200,13 +201,11 @@ .nav-open { .page-wrapper { - left: 80%; left: calc(~'100% - @{active-nav-indent}'); } .nav-sections { @_shadow: 0 0 5px 0 rgba(50, 50, 50, .75); - .lib-css(box-shadow, @_shadow, 1); left: 0; z-index: 99; @@ -293,10 +292,6 @@ display: none; } - .nav-sections-item-content { - display: block !important; - } - .nav-sections-item-content > * { display: none; } diff --git a/composer.json b/composer.json index 1af86e438882c..57fbfaaa35c2b 100644 --- a/composer.json +++ b/composer.json @@ -222,6 +222,8 @@ "magento/module-media-gallery-cms-ui": "*", "magento/module-media-gallery-catalog-integration": "*", "magento/module-media-gallery-catalog": "*", + "magento/module-media-gallery-renditions": "*", + "magento/module-media-gallery-renditions-api": "*", "magento/module-media-storage": "*", "magento/module-message-queue": "*", "magento/module-msrp": "*", diff --git a/composer.lock b/composer.lock index 551167152be4d..8a5d82536cee4 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": "aadcf8a265dd7ecbb86dd3dd4e49bc28", + "content-hash": "a03edc1c8ee05f82886eebd6ed288df8", "packages": [ { "name": "colinmollenhour/cache-backend-file", diff --git a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php index 7a4f472c69513..538c0b0ee5fac 100644 --- a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php @@ -225,6 +225,7 @@ public function testUpdateBundleAddSelection() public function testUpdateBundleAddAndDeleteOption() { $bundleProduct = $this->createDynamicBundleProduct(); + $linkedProductPrice = 20; $bundleProductOptions = $this->getBundleProductOptions($bundleProduct); @@ -238,7 +239,7 @@ public function testUpdateBundleAddAndDeleteOption() [ 'sku' => 'simple2', 'qty' => 2, - "price" => 20, + "price" => $linkedProductPrice, "price_type" => 1, "is_default" => false, ], @@ -256,6 +257,7 @@ public function testUpdateBundleAddAndDeleteOption() $this->assertFalse(isset($bundleOptions[1])); $this->assertEquals('simple2', $bundleOptions[0]['product_links'][0]['sku']); $this->assertEquals(2, $bundleOptions[0]['product_links'][0]['qty']); + $this->assertEquals($linkedProductPrice, $bundleOptions[0]['product_links'][0]['price']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php index c2f94128ef8ec..cb210b180682c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php @@ -10,7 +10,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Get related products test + * Test coverage for get related products */ class GetRelatedProductsTest extends GraphQlAbstract { @@ -49,6 +49,40 @@ public function testQueryRelatedProducts() self::assertRelatedProducts($relatedProducts); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_related_disabled.php + */ + public function testQueryDisableRelatedProduct() + { + $productSku = 'simple_with_cross'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + related_products + { + sku + name + url_key + created_at + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertCount(1, $response['products']['items']); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertArrayHasKey('related_products', $response['products']['items'][0]); + $relatedProducts = $response['products']['items'][0]['related_products']; + self::assertCount(0, $relatedProducts); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/products_crosssell.php */ diff --git a/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php b/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php new file mode 100644 index 0000000000000..85007ad560d53 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Core\Version; + +/** + * Class for magento version flag. + */ +class View +{ + /** + * Returns flag that checks that magento version is clean community version. + * + * @return bool + */ + public function isVersionUpdated(): bool + { + return false; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/OptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/OptionsTest.php new file mode 100644 index 0000000000000..c50c21a3328ae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/OptionsTest.php @@ -0,0 +1,621 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Block\Product\View\Options\AbstractRenderCustomOptionsTest; +use Magento\Catalog\Helper\Product as HelperProduct; +use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Option\Value; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\TestFramework\Helper\Xpath; + +/** + * Test cases related to check that simple product custom option renders as expected. + * + * @magentoAppArea adminhtml + */ +class OptionsTest extends AbstractRenderCustomOptionsTest +{ + /** @var HelperProduct */ + private $helperProduct; + + /** @var DataObjectFactory */ + private $dataObjectFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->helperProduct = $this->objectManager->get(HelperProduct::class); + $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php + * @return void + */ + public function testRenderCustomOptionsWithoutOptions(): void + { + $product = $this->productRepository->get('simple'); + $this->assertEquals( + 0, + Xpath::getElementsCountForXpath( + "//fieldset[@id='product_composite_configure_fields_options']", + $this->getOptionHtml($product) + ), + 'The option block is expected to be empty!' + ); + } + + /** + * Check that options from text group(field, area) render as expected. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php + * @dataProvider renderCustomOptionsFromTextGroupProvider + * @param array $optionData + * @param array $checkArray + * @return void + */ + public function testRenderCustomOptionsFromTextGroup(array $optionData, array $checkArray): void + { + $this->assertTextOptionRenderingOnProduct('simple', $optionData, $checkArray); + } + + /** + * Provides test data to verify the display of text type options. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function renderCustomOptionsFromTextGroupProvider(): array + { + return [ + 'type_text_required_field' => [ + [ + Option::KEY_TITLE => 'Test option type text 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 0, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'block_with_required_class' => '<div class="field admin__field">', + 'title' => 'Test option type text 1', + ], + 'equals_xpath' => [ + 'zero_price' => [ + 'xpath' => "//label[contains(@class, 'admin__field-label')]/span", + 'message' => 'Expected empty price is incorrect or missing!', + 'expected' => 0, + ], + ], + ], + ], + 'type_text_is_required_option' => [ + [ + Option::KEY_TITLE => 'Test option type text 2', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 1, + Option::KEY_PRICE => 0, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'block_with_required_class' => '<div class="field admin__field required _required">', + ], + ], + ], + 'type_text_fixed_positive_price' => [ + [ + Option::KEY_TITLE => 'Test option type text 3', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 50, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'price' => 'data-price-amount="50"', + ], + 'equals_xpath' => [ + 'sign_price' => [ + 'xpath' => "//label[contains(@class, 'admin__field-label')]/span[contains(text(), '+')]", + 'message' => 'Expected positive price is incorrect or missing!', + ], + ], + ], + ], + 'type_text_fixed_negative_price' => [ + [ + Option::KEY_TITLE => 'Test option type text 4', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => -50, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'price' => 'data-price-amount="50"', + ], + 'equals_xpath' => [ + 'sign_price' => [ + 'xpath' => "//label[contains(@class, 'admin__field-label')]/span[contains(text(), '-')]", + 'message' => 'Expected negative price is incorrect or missing!', + ], + ], + ], + ], + 'type_text_percent_price' => [ + [ + Option::KEY_TITLE => 'Test option type text 5', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 50, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_PERCENT, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'price' => 'data-price-amount="5"', + ], + ], + ], + 'type_text_max_characters' => [ + [ + Option::KEY_TITLE => 'Test option type text 6', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 10, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 99, + ], + [ + 'max_characters' => (string)__('Maximum number of characters:') . ' <strong>99</strong>', + ], + ], + 'type_field' => [ + [ + Option::KEY_TITLE => 'Test option type field 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 10, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + 'configure_option_value' => 'Type field option value', + ], + [ + 'equals_xpath' => [ + 'control_price_attribute' => [ + 'xpath' => "//input[@id='options_%s_text' and @price='%s']", + 'message' => 'Expected input price is incorrect or missing!', + ], + 'default_option_value' => [ + 'xpath' => "//input[@id='options_%s_text' and @value='Type field option value']", + 'message' => 'Expected input default value is incorrect or missing!', + ], + ], + ], + ], + 'type_area' => [ + [ + Option::KEY_TITLE => 'Test option type area 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_AREA, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 10, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + 'configure_option_value' => 'Type area option value', + ], + [ + 'equals_xpath' => [ + 'control_price_attribute' => [ + 'xpath' => "//textarea[@id='options_%s_text' and @price='%s']", + 'message' => 'Expected textarea price is incorrect or missing!', + ], + 'default_option_value' => [ + 'xpath' => "//textarea[@id='options_%s_text' " + . "and contains(text(), 'Type area option value')]", + 'message' => 'Expected textarea default value is incorrect or missing!', + ], + ], + ], + ], + ]; + } + + /** + * Check that options from select group(drop-down, radio buttons, checkbox, multiple select) render as expected. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php + * @dataProvider renderCustomOptionsFromSelectGroupProvider + * @param array $optionData + * @param array $optionValueData + * @param array $checkArray + * @return void + */ + public function testRenderCustomOptionsFromSelectGroup( + array $optionData, + array $optionValueData, + array $checkArray + ): void { + $this->assertSelectOptionRenderingOnProduct('simple', $optionData, $optionValueData, $checkArray); + } + + /** + * Provides test data to verify the display of select type options. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function renderCustomOptionsFromSelectGroupProvider(): array + { + return [ + 'type_select_required_field' => [ + [ + Option::KEY_TITLE => 'Test option type select 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + Option::KEY_IS_REQUIRE => 0, + ], + [ + Value::KEY_TITLE => 'Select value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'contains' => [ + 'block_with_required_class' => '<div class="admin__field field">', + 'title' => '<span>Test option type select 1</span>', + ], + 'equals_xpath' => [ + 'required_element' => [ + 'xpath' => "//select[@id='select_%s']", + 'message' => 'Expected select type is incorrect or missing!', + ], + ], + ], + ], + 'type_select_is_required_option' => [ + [ + Option::KEY_TITLE => 'Test option type select 2', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + Option::KEY_IS_REQUIRE => 1, + ], + [ + Value::KEY_TITLE => 'Select value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'contains' => [ + 'block_with_required_class' => '<div class="admin__field field _required">', + ], + ], + ], + 'type_drop_down_with_selected' => [ + [ + Option::KEY_TITLE => 'Test option type drop-down 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + Option::KEY_IS_REQUIRE => 0, + 'configure_option_value' => 'Drop-down value 1', + ], + [ + Value::KEY_TITLE => 'Drop-down value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'element_type' => [ + 'xpath' => "//select[contains(@class, 'admin__control-select')]", + 'message' => 'Expected drop down type is incorrect or missing!', + ], + 'default_value' => [ + 'xpath' => "//option[contains(text(), '" . __('-- Please Select --') . "')]", + 'message' => 'Expected default value is incorrect or missing!', + ], + 'selected_value' => [ + 'xpath' => "//option[@selected='selected' and contains(text(), 'Drop-down value 1')]", + 'message' => 'Expected selected value is incorrect or missing!', + ], + ], + ], + ], + 'type_multiple_with_selected' => [ + [ + Option::KEY_TITLE => 'Test option type multiple 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE, + Option::KEY_IS_REQUIRE => 0, + 'configure_option_value' => 'Multiple value 1', + ], + [ + Value::KEY_TITLE => 'Multiple value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'element_type' => [ + 'xpath' => "//select[contains(@class, 'admin__control-multiselect') " + . "and @multiple='multiple']", + 'message' => 'Expected multiple type is incorrect or missing!', + ], + 'selected_value' => [ + 'xpath' => "//option[@selected='selected' and contains(text(), 'Multiple value 1')]", + 'message' => 'Expected selected value is incorrect or missing!', + ], + ], + ], + ], + 'type_checkable_required_field' => [ + [ + Option::KEY_TITLE => 'Test option type checkable 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO, + Option::KEY_IS_REQUIRE => 0, + ], + [ + Value::KEY_TITLE => 'Checkable value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'required_checkable_option' => [ + 'xpath' => "//div[@id='options-%s-list']", + 'message' => 'Expected checkable option is incorrect or missing!', + ], + 'option_value_title' => [ + 'xpath' => "//label[@for='options_%s_2']/span[contains(text(), 'Checkable value 1')]", + 'message' => 'Expected option value title is incorrect or missing!', + ], + ], + ], + ], + 'type_radio_is_required_option' => [ + [ + Option::KEY_TITLE => 'Test option type radio 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO, + Option::KEY_IS_REQUIRE => 1, + ], + [ + Value::KEY_TITLE => 'Radio value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'span_container' => [ + 'xpath' => "//span[@id='options-%s-container']", + 'message' => 'Expected span container is incorrect or missing!', + ], + 'default_option_value' => [ + 'xpath' => "//label[@for='options_%s']/span[contains(text(), '" . __('None') . "')]", + 'message' => 'Expected default option value is incorrect or missing!', + 'expected' => 0, + ], + ], + ], + ], + 'type_radio_with_selected' => [ + [ + Option::KEY_TITLE => 'Test option type radio 2', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO, + Option::KEY_IS_REQUIRE => 0, + 'configure_option_value' => 'Radio value 1', + ], + [ + Value::KEY_TITLE => 'Radio value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'default_option_value' => [ + 'xpath' => "//label[@for='options_%s']/span[contains(text(), '" . __('None') . "')]", + 'message' => 'Expected default option value is incorrect or missing!', + ], + 'element_type' => [ + 'xpath' => "//input[@id='options_%s_2' and contains(@class, 'admin__control-radio')]", + 'message' => 'Expected radio type is incorrect or missing!', + ], + 'selected_value' => [ + 'xpath' => "//input[@id='options_%s_2' and @checked='checked']", + 'message' => 'Expected selected option value is incorrect or missing!', + ], + ], + ], + ], + 'type_checkbox_is_required_option' => [ + [ + Option::KEY_TITLE => 'Test option type checkbox 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, + Option::KEY_IS_REQUIRE => 1, + ], + [ + Value::KEY_TITLE => 'Checkbox value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Value::KEY_SKU => '', + ], + [ + 'equals_xpath' => [ + 'span_container' => [ + 'xpath' => "//span[@id='options-%s-container']", + 'message' => 'Expected span container is incorrect or missing!', + ], + ], + ], + ], + 'type_checkbox_with_selected' => [ + [ + Option::KEY_TITLE => 'Test option type checkbox 2', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, + Option::KEY_IS_REQUIRE => 0, + 'configure_option_value' => 'Checkbox value 1', + ], + [ + Value::KEY_TITLE => 'Checkbox value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Value::KEY_SKU => '', + ], + [ + 'equals_xpath' => [ + 'element_type' => [ + 'xpath' => "//input[@id='options_%s_2' and contains(@class, 'admin__control-checkbox')]", + 'message' => 'Expected checkbox type is incorrect or missing!', + ], + 'selected_value' => [ + 'xpath' => "//input[@id='options_%s_2' and @checked='checked']", + 'message' => 'Expected selected option value is incorrect or missing!', + ], + ], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + protected function addOptionToProduct( + ProductInterface $product, + array $optionData, + array $optionValueData = [] + ): ProductInterface { + $product = parent::addOptionToProduct($product, $optionData, $optionValueData); + + if (isset($optionData['configure_option_value'])) { + $optionValue = $optionData['configure_option_value']; + $option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]); + if (!empty($optionValueData)) { + $optionValueObject = $this->findOptionValueByTitle($option, $optionValue); + $optionValue = $option->getType() === Option::OPTION_TYPE_CHECKBOX + ? [$optionValueObject->getOptionTypeId()] + : $optionValueObject->getOptionTypeId(); + } + /** @var DataObject $request */ + $buyRequest = $this->dataObjectFactory->create(); + $buyRequest->setData([ + 'qty' => 1, + 'options' => [$option->getId() => $optionValue], + ]); + $this->helperProduct->prepareProductOptions($product, $buyRequest); + } + + return $product; + } + + /** + * @inheritdoc + */ + protected function baseOptionAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + if (isset($checkArray['contains'])) { + foreach ($checkArray['contains'] as $needle) { + $this->assertStringContainsString($needle, $optionHtml); + } + } + } + + /** + * @inheritdoc + */ + protected function additionalTypeTextAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + parent::additionalTypeTextAsserts($option, $optionHtml, $checkArray); + + if (isset($checkArray['equals_xpath'])) { + foreach ($checkArray['equals_xpath'] as $key => $value) { + $value['args'] = $key === 'control_price_attribute' ? [(float)$option->getPrice()] : []; + $this->assertEqualsXpath($option, $optionHtml, $value); + } + } + } + + /** + * @inheritdoc + */ + protected function additionalTypeSelectAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + parent::additionalTypeSelectAsserts($option, $optionHtml, $checkArray); + + if (isset($checkArray['equals_xpath'])) { + foreach ($checkArray['equals_xpath'] as $value) { + $this->assertEqualsXpath($option, $optionHtml, $value); + } + } + } + + /** + * @inheritdoc + */ + protected function getHandlesList(): array + { + return [ + 'default', + 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE', + 'catalog_product_view_type_simple', + ]; + } + + /** + * @inheritdoc + */ + protected function getMaxCharactersCssClass(): string + { + return 'class="note"'; + } + + /** + * @inheritdoc + */ + protected function getOptionsBlockName(): string + { + return 'product.composite.fieldset.options'; + } + + /** + * Checks that the xpath string is equal to the expected value + * + * @param ProductCustomOptionInterface $option + * @param string $html + * @param array $xpathData + * @return void + */ + private function assertEqualsXpath(ProductCustomOptionInterface $option, string $html, array $xpathData): void + { + $args = array_merge([$option->getOptionId()], $xpathData['args'] ?? []); + $expected = $xpathData['expected'] ?? 1; + $this->assertEquals( + $expected, + Xpath::getElementsCountForXpath(sprintf($xpathData['xpath'], ...$args), $html), + $xpathData['message'] + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php new file mode 100644 index 0000000000000..a51b51a73645f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Product as HelperProduct; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test Qty block in composite product configuration layout + * + * @see \Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset\Qty + * @magentoAppArea adminhtml + */ +class QtyTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Qty */ + private $block; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var Registry */ + private $registry; + + /** @var HelperProduct */ + private $helperProduct; + + /** @var DataObjectFactory */ + private $dataObjectFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Qty::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->helperProduct = $this->objectManager->get(HelperProduct::class); + $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_product'); + $this->registry->unregister('product'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testGetProduct(): void + { + $product = $this->productRepository->get('simple-1'); + $this->registerProduct($product); + $this->assertEquals( + $product->getId(), + $this->block->getProduct()->getId(), + 'The expected product is missing in the Qty block!' + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @dataProvider getQtyValueProvider + * @param bool $isQty + * @param int $qty + * @return void + */ + public function testGetQtyValue(bool $isQty = false, int $qty = 1): void + { + $product = $this->productRepository->get('simple-1'); + if ($isQty) { + /** @var DataObject $request */ + $buyRequest = $this->dataObjectFactory->create(); + $buyRequest->setData(['qty' => $qty]); + $this->helperProduct->prepareProductOptions($product, $buyRequest); + } + $this->registerProduct($product); + $this->assertEquals($qty, $this->block->getQtyValue(), 'Expected block qty value is incorrect!'); + } + + /** + * Provides test data to verify block qty value. + * + * @return array + */ + public function getQtyValueProvider(): array + { + return [ + 'with_qty' => [ + 'is_qty' => true, + 'qty' => 5, + ], + 'without_qty' => [], + ]; + } + + /** + * Register the product + * + * @param ProductInterface $product + * @return void + */ + private function registerProduct(ProductInterface $product): void + { + $this->registry->unregister('current_product'); + $this->registry->unregister('product'); + $this->registry->register('current_product', $product); + $this->registry->register('product', $product); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php new file mode 100644 index 0000000000000..ab09314e18cc8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Adminhtml\Product\Composite; + +use Magento\Backend\Model\View\Result\Page; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Test Fieldset block in composite product configuration layout + * + * @see \Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset + * @magentoAppArea adminhtml + */ +class FieldsetTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Page */ + private $page; + + /** @var Registry */ + private $registry; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var string */ + private $fieldsetXpath = "//fieldset[@id='product_composite_configure_fields_%s']"; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->page = $this->objectManager->get(PageFactory::class)->create(); + $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('current_product'); + $this->registry->unregister('product'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @return void + */ + public function testRenderHtml(): void + { + $product = $this->productRepository->get('simple'); + $this->registerProduct($product); + $this->preparePage(); + $fieldsetBlock = $this->page->getLayout()->getBlock('product.composite.fieldset'); + $this->assertNotFalse($fieldsetBlock, 'Expected fieldset block is missing!'); + $html = $fieldsetBlock->toHtml(); + + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($this->fieldsetXpath, 'options'), $html), + 'Expected options block is missing!' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($this->fieldsetXpath, 'qty'), $html), + 'Expected qty block is missing!' + ); + } + + /** + * Prepare page layout + * + * @return void + */ + private function preparePage(): void + { + $this->page->addHandle([ + 'default', + 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE', + 'catalog_product_view_type_simple', + ]); + $this->page->getLayout()->generateXml(); + } + + /** + * Register the product + * + * @param ProductInterface $product + * @return void + */ + private function registerProduct(ProductInterface $product): void + { + $this->registry->unregister('current_product'); + $this->registry->unregister('product'); + $this->registry->register('current_product', $product); + $this->registry->register('product', $product); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php index eb34696c70dbf..b575fc5e7033c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php @@ -9,14 +9,14 @@ use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\View\Options; use Magento\Catalog\Model\Product\Option; -use Magento\Catalog\Model\Product\Option\Value; -use Magento\Framework\View\Element\Template; use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\TestCase; @@ -29,12 +29,12 @@ abstract class AbstractRenderCustomOptionsTest extends TestCase /** * @var ObjectManager */ - private $objectManager; + protected $objectManager; /** * @var ProductRepositoryInterface */ - private $productRepository; + protected $productRepository; /** * @var ProductCustomOptionInterfaceFactory @@ -57,12 +57,13 @@ abstract class AbstractRenderCustomOptionsTest extends TestCase protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); - $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); $this->productCustomOptionFactory = $this->objectManager->get(ProductCustomOptionInterfaceFactory::class); $this->productCustomOptionValuesFactory = $this->objectManager->get( ProductCustomOptionValuesInterfaceFactory::class ); - $this->page = $this->objectManager->create(Page::class); + $this->page = $this->objectManager->get(PageFactory::class)->create(); parent::setUp(); } @@ -94,11 +95,26 @@ protected function assertTextOptionRenderingOnProduct( $option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]); $optionHtml = $this->getOptionHtml($product); $this->baseOptionAsserts($option, $optionHtml, $checkArray); + $this->additionalTypeTextAsserts($option, $optionHtml, $checkArray); + } - if ($optionData[Option::KEY_MAX_CHARACTERS] > 0) { + /** + * Additional asserts for rendering text type options. + * + * @param ProductCustomOptionInterface $option + * @param string $optionHtml + * @param array $checkArray + * @return void + */ + protected function additionalTypeTextAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + if ($option->getMaxCharacters() > 0) { $this->assertStringContainsString($checkArray['max_characters'], $optionHtml); } else { - $this->assertStringNotContainsString('class="character-counter', $optionHtml); + $this->assertStringNotContainsString($this->getMaxCharactersCssClass(), $optionHtml); } } @@ -153,22 +169,36 @@ protected function assertSelectOptionRenderingOnProduct( $product = $this->productRepository->get($productSku); $product = $this->addOptionToProduct($product, $optionData, $optionValueData); $option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]); - $optionValues = $option->getValues(); - $optionValue = reset($optionValues); $optionHtml = $this->getOptionHtml($product); $this->baseOptionAsserts($option, $optionHtml, $checkArray); + $this->additionalTypeSelectAsserts($option, $optionHtml, $checkArray); + } + /** + * Additional asserts for rendering select type options. + * + * @param ProductCustomOptionInterface $option + * @param string $optionHtml + * @param array $checkArray + * @return void + */ + protected function additionalTypeSelectAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + $optionValues = $option->getValues(); + $optionValue = reset($optionValues); if (isset($checkArray['not_contain_arr'])) { foreach ($checkArray['not_contain_arr'] as $notContainPattern) { $this->assertDoesNotMatchRegularExpression($notContainPattern, $optionHtml); } } - if (isset($checkArray['option_value_item'])) { $checkArray['option_value_item'] = sprintf( $checkArray['option_value_item'], $optionValue->getOptionTypeId(), - $optionValueData[Value::KEY_TITLE] + $optionValue->getTitle() ); $this->assertMatchesRegularExpression($checkArray['option_value_item'], $optionHtml); } @@ -284,7 +314,7 @@ protected function assertDateOptionRenderingOnProduct( * @param array $checkArray * @return void */ - private function baseOptionAsserts( + protected function baseOptionAsserts( ProductCustomOptionInterface $option, string $optionHtml, array $checkArray @@ -317,7 +347,7 @@ private function baseOptionAsserts( * @param array $optionValueData * @return ProductInterface */ - private function addOptionToProduct( + protected function addOptionToProduct( ProductInterface $product, array $optionData, array $optionValueData = [] @@ -341,28 +371,16 @@ private function addOptionToProduct( * @param ProductInterface $product * @return string */ - private function getOptionHtml(ProductInterface $product): string - { - $optionsBlock = $this->getOptionsBlock(); - $optionsBlock->setProduct($product); - - return $optionsBlock->toHtml(); - } - - /** - * Get options block. - * - * @return Options - */ - private function getOptionsBlock(): Options + protected function getOptionHtml(ProductInterface $product): string { $this->page->addHandle($this->getHandlesList()); $this->page->getLayout()->generateXml(); - /** @var Template $productInfoFormOptionsBlock */ - $productInfoFormOptionsBlock = $this->page->getLayout()->getBlock('product.info.form.options'); - $optionsWrapperBlock = $productInfoFormOptionsBlock->getChildBlock('product_options_wrapper'); + /** @var Options $optionsBlock */ + $optionsBlock = $this->page->getLayout()->getBlock($this->getOptionsBlockName()); + $this->assertNotFalse($optionsBlock); + $optionsBlock->setProduct($product); - return $optionsWrapperBlock->getChildBlock('product_options'); + return $optionsBlock->toHtml(); } /** @@ -372,7 +390,7 @@ private function getOptionsBlock(): Options * @param string $optionTitle * @return null|Option */ - private function findOptionByTitle(ProductInterface $product, string $optionTitle): ?Option + protected function findOptionByTitle(ProductInterface $product, string $optionTitle): ?Option { $option = null; foreach ($product->getOptions() as $customOption) { @@ -385,10 +403,42 @@ private function findOptionByTitle(ProductInterface $product, string $optionTitl return $option; } + /** + * Find and return custom option value. + * + * @param ProductCustomOptionInterface $option + * @param string $optionValueTitle + * @return null|ProductCustomOptionValuesInterface + */ + protected function findOptionValueByTitle( + ProductCustomOptionInterface $option, + string $optionValueTitle + ): ?ProductCustomOptionValuesInterface { + $optionValue = null; + foreach ($option->getValues() as $customOptionValue) { + if ($customOptionValue->getTitle() === $optionValueTitle) { + $optionValue = $customOptionValue; + break; + } + } + + return $optionValue; + } + /** * Return all need handles for load. * * @return array */ abstract protected function getHandlesList(): array; + + /** + * @return string + */ + abstract protected function getMaxCharactersCssClass(): string; + + /** + * @return string + */ + abstract protected function getOptionsBlockName(): string; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php index da31cfc74476a..83c249ed062e6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php @@ -10,7 +10,6 @@ /** * Test cases related to check that simple product custom option renders as expected. * - * @magentoDbIsolation disabled * @magentoAppArea frontend */ class RenderOptionsTest extends AbstractRenderCustomOptionsTest @@ -89,4 +88,20 @@ protected function getHandlesList(): array 'catalog_product_view', ]; } + + /** + * @inheritdoc + */ + protected function getMaxCharactersCssClass(): string + { + return 'class="character-counter'; + } + + /** + * @inheritdoc + */ + protected function getOptionsBlockName(): string + { + return 'product.info.options'; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php new file mode 100644 index 0000000000000..68d5c43434daa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple Related Product') + ->setSku('simple') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) + ->setWebsiteIds([1]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) + ->save(); + +/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ +$productLink = $objectManager->create(\Magento\Catalog\Api\Data\ProductLinkInterface::class); +$productLink->setSku('simple_with_cross'); +$productLink->setLinkedProductSku('simple'); +$productLink->setPosition(1); +$productLink->setLinkType('related'); + +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple Product With Related Product') + ->setSku('simple_with_cross') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) + ->setProductLinks([$productLink]) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php new file mode 100644 index 0000000000000..958398660b132 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$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); + +try { + $firstProduct = $productRepository->get('simple', false, null, true); + $productRepository->delete($firstProduct); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +try { + $secondProduct = $productRepository->get('simple_with_cross', false, null, true); + $productRepository->delete($secondProduct); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php new file mode 100644 index 0000000000000..5cca93ce3478c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Type\Onepage; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var AddressInterface $quoteShippingAddress */ +$quoteShippingAddress = $objectManager->get(AddressInterfaceFactory::class)->create(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = $objectManager->get(AddressRepositoryInterface::class); +$quoteShippingAddress->importCustomerAddressData($addressRepository->getById(1)); +$customer = $customerRepository->getById(1); + +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId(1) + ->setIsActive(true) + ->setIsMultiShipping(0) + ->assignCustomerWithAddressChange($customer) + ->setShippingAddress($quoteShippingAddress) + ->setBillingAddress($quoteShippingAddress) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('55555555') + ->setEmail($customer->getEmail()); +$quote->addProduct($productRepository->get('simple-1'), 55); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getShippingAddress()->collectShippingRates(); +$quote->getPayment()->setMethod('checkmo'); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php new file mode 100644 index 0000000000000..a599d008cf89c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('55555555'); +if ($quote) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.php new file mode 100644 index 0000000000000..c74e76f74115f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Type\Onepage; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$customer = $customerRepository->get('customer@example.com'); + +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId(1) + ->setIsActive(false) + ->setIsMultiShipping(0) + ->setCustomer($customer) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('test_order_with_customer_inactive_quote') + ->addProduct($productRepository->get('taxable_product'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.php new file mode 100644 index 0000000000000..d45cbb547d29d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('test_order_with_customer_inactive_quote'); +if ($quote !== null) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php new file mode 100644 index 0000000000000..88c8fb726c472 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test Configurable block in composite product configuration layout + * + * @see \Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset\Configurable + * @magentoAppArea adminhtml + */ +class ConfigurableTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var SerializerInterface */ + private $serializer; + + /** @var Configurable */ + private $block; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var Registry */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->serializer = $this->objectManager->get(SerializerInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Configurable::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('product'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testGetProduct(): void + { + $product = $this->productRepository->get('simple-1'); + $this->registerProduct($product); + $blockProduct = $this->block->getProduct(); + $this->assertSame($product, $blockProduct); + $this->assertEquals( + $product->getId(), + $blockProduct->getId(), + 'The expected product is missing in the Configurable block!' + ); + $this->assertNotNull($blockProduct->getTypeInstance()->getStoreFilter($blockProduct)); + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @return void + */ + public function testGetJsonConfig(): void + { + $product = $this->productRepository->get('configurable'); + $this->registerProduct($product); + $config = $this->serializer->unserialize($this->block->getJsonConfig()); + $this->assertTrue($config['disablePriceReload']); + $this->assertTrue($config['stablePrices']); + } + + /** + * Register the product + * + * @param ProductInterface $product + * @return void + */ + private function registerProduct(ProductInterface $product): void + { + $this->registry->unregister('product'); + $this->registry->register('product', $product); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php index 55f8b91f07093..303a32d34bf6c 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php @@ -12,7 +12,6 @@ /** * Test cases related to check that configurable product custom option renders as expected. * - * @magentoDbIsolation disabled * @magentoAppArea frontend */ class RenderOptionsTest extends AbstractRenderCustomOptionsTest @@ -93,4 +92,20 @@ protected function getHandlesList(): array 'catalog_product_view_type_configurable', ]; } + + /** + * @inheritdoc + */ + protected function getMaxCharactersCssClass(): string + { + return 'class="character-counter'; + } + + /** + * @inheritdoc + */ + protected function getOptionsBlockName(): string + { + return 'product.info.options'; + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php index 39ed7965ea9e9..0344d467a3cc2 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php @@ -9,9 +9,13 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Product as HelperProduct; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\View\LayoutInterface; @@ -26,6 +30,7 @@ * @magentoAppIsolation enabled * @magentoDbIsolation enabled * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigurableTest extends TestCase { @@ -64,6 +69,14 @@ class ConfigurableTest extends TestCase */ private $product; + /** + * @var HelperProduct + */ + private $helperProduct; + + /** @var DataObjectFactory */ + private $dataObjectFactory; + /** * @inheritdoc */ @@ -79,6 +92,8 @@ protected function setUp(): void $this->product = $this->productRepository->get('configurable'); $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Configurable::class); $this->block->setProduct($this->product); + $this->helperProduct = $this->objectManager->get(HelperProduct::class); + $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class); } /** @@ -128,6 +143,29 @@ public function testGetJsonConfig(): void $this->assertCount(0, $config['images']); } + /** + * @return void + */ + public function testGetJsonConfigWithPreconfiguredValues(): void + { + /** @var ConfigurableAttribute $attribute */ + $attribute = $this->product->getExtensionAttributes()->getConfigurableProductOptions()[0]; + $expectedAttributeValue = [ + $attribute->getAttributeId() => $attribute->getOptions()[0]['value_index'], + ]; + /** @var DataObject $request */ + $buyRequest = $this->dataObjectFactory->create(); + $buyRequest->setData([ + 'qty' => 1, + 'super_attribute' => $expectedAttributeValue, + ]); + $this->helperProduct->prepareProductOptions($this->product, $buyRequest); + + $config = $this->serializer->unserialize($this->block->getJsonConfig()); + $this->assertArrayHasKey('defaultValues', $config); + $this->assertEquals($expectedAttributeValue, $config['defaultValues']); + } + /** * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_child_products_with_images.php * @return void diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php index 9c382068ceebc..12585992d084c 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php @@ -3,126 +3,175 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Block\Address; +use Magento\Customer\Model\AddressRegistry; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + /** * Tests Address Edit Block + * + * @magentoAppArea frontend + * @magentoAppIsolation enabled */ -class EditTest extends \PHPUnit\Framework\TestCase +class EditTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + /** @var Edit */ - protected $_block; + private $block; - /** @var \Magento\Customer\Model\Session */ - protected $_customerSession; + /** @var Session */ + private $customerSession; - /** @var \Magento\Backend\Block\Template\Context */ - protected $_context; + /** @var AddressRegistry */ + private $addressRegistry; - /** @var string */ - protected $_requestId; + /** @var CustomerRegistry */ + private $customerRegistry; + /** @var RequestInterface */ + private $request; + + /** + * @inheritdoc + */ protected function setUp(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $this->_customerSession = $objectManager->get(\Magento\Customer\Model\Session::class); - $this->_customerSession->setCustomerId(1); - - $this->_context = $objectManager->get(\Magento\Backend\Block\Template\Context::class); - $this->_requestId = $this->_context->getRequest()->getParam('id'); - $this->_context->getRequest()->setParam('id', '1'); - - $objectManager->get(\Magento\Framework\App\State::class)->setAreaCode('frontend'); - - /** @var $layout \Magento\Framework\View\Layout */ - $layout = $objectManager->get(\Magento\Framework\View\LayoutInterface::class); - $currentCustomer = $objectManager->create( - \Magento\Customer\Helper\Session\CurrentCustomer::class, - ['customerSession' => $this->_customerSession] - ); - $this->_block = $layout->createBlock( - \Magento\Customer\Block\Address\Edit::class, - '', - ['customerSession' => $this->_customerSession, 'currentCustomer' => $currentCustomer] - ); + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(Session::class); + $this->customerSession->setCustomerId(1); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->request->setParam('id', '1'); + /** @var Page $page */ + $page = $this->objectManager->get(PageFactory::class)->create(); + $page->addHandle(['default', 'customer_address_form']); + $page->getLayout()->generateXml(); + $this->block = $page->getLayout()->getBlock('customer_address_edit'); + $this->addressRegistry = $this->objectManager->get(AddressRegistry::class); + $this->customerRegistry = $this->objectManager->get(CustomerRegistry::class); } + /** + * @inheritdoc + */ protected function tearDown(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_customerSession->setCustomerId(null); - $this->_context->getRequest()->setParam('id', $this->_requestId); - /** @var \Magento\Customer\Model\AddressRegistry $addressRegistry */ - $addressRegistry = $objectManager->get(\Magento\Customer\Model\AddressRegistry::class); + parent::tearDown(); + $this->customerSession->setCustomerId(null); + $this->request->setParam('id', null); //Cleanup address from registry - $addressRegistry->remove(1); - $addressRegistry->remove(2); - - /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ - $customerRegistry = $objectManager->get(\Magento\Customer\Model\CustomerRegistry::class); + $this->addressRegistry->remove(1); + $this->addressRegistry->remove(2); //Cleanup customer from registry - $customerRegistry->remove(1); + $this->customerRegistry->remove(1); } /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void */ - public function testGetSaveUrl() + public function testGetSaveUrl(): void { - $this->assertEquals('http://localhost/index.php/customer/address/formPost/', $this->_block->getSaveUrl()); + $this->assertEquals('http://localhost/index.php/customer/address/formPost/', $this->block->getSaveUrl()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @return void */ - public function testGetRegionId() + public function testGetRegionId(): void { - $this->assertEquals(1, $this->_block->getRegionId()); + $this->assertEquals(1, $this->block->getRegionId()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @return void */ - public function testGetCountryId() + public function testGetCountryId(): void { - $this->assertEquals('US', $this->_block->getCountryId()); + $this->assertEquals('US', $this->block->getCountryId()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php + * @return void */ - public function testGetCustomerAddressCount() + public function testGetCustomerAddressCount(): void { - $this->assertEquals(2, $this->_block->getCustomerAddressCount()); + $this->assertEquals(2, $this->block->getCustomerAddressCount()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void */ - public function testCanSetAsDefaultShipping() + public function testCanSetAsDefaultShipping(): void { - $this->assertEquals(0, $this->_block->canSetAsDefaultShipping()); + $this->assertEquals(0, $this->block->canSetAsDefaultShipping()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void */ - public function testIsDefaultBilling() + public function testIsDefaultBilling(): void { - $this->assertFalse($this->_block->isDefaultBilling()); + $this->assertFalse($this->block->isDefaultBilling()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @return void + */ + public function testGetStreetLine(): void + { + $this->assertEquals('Green str, 67', $this->block->getStreetLine(1)); + $this->assertEquals('', $this->block->getStreetLine(2)); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/create_account/vat_frontend_visibility 1 + * @return void + */ + public function testVatIdFieldVisible(): void + { + $html = $this->block->toHtml(); + $labelXpath = "//div[contains(@class, 'taxvat')]//label/span[normalize-space(text()) = '%s']"; + $this->assertEquals(1, Xpath::getElementsCountForXpath(sprintf($labelXpath, __('VAT Number')), $html)); + $inputXpath = "//div[contains(@class, 'taxvat')]//div/input[contains(@id,'vat_id') and @type='text']"; + $this->assertEquals(1, Xpath::getElementsCountForXpath($inputXpath, $html)); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/create_account/vat_frontend_visibility 0 + * @return void */ - public function testGetStreetLine() + public function testVatIdFieldNotVisible(): void { - $this->assertEquals('Green str, 67', $this->_block->getStreetLine(1)); - $this->assertEquals('', $this->_block->getStreetLine(2)); + $html = $this->block->toHtml(); + $labelXpath = "//div[contains(@class, 'taxvat')]//label/span[normalize-space(text()) = '%s']"; + $this->assertEquals(0, Xpath::getElementsCountForXpath(sprintf($labelXpath, __('VAT Number')), $html)); + $inputXpath = "//div[contains(@class, 'taxvat')]//div/input[contains(@id,'vat_id') and @type='text']"; + $this->assertEquals(0, Xpath::getElementsCountForXpath($inputXpath, $html)); } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php new file mode 100644 index 0000000000000..8ce1d2ae9ccf9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php @@ -0,0 +1,303 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Account; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\App\Http; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; +use Magento\Theme\Controller\Result\MessagePlugin; + +/** + * Tests from customer account create post action. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreatePostTest extends AbstractController +{ + /** + * @var TransportBuilderMock + */ + private $transportBuilderMock; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CookieManagerInterface + */ + private $cookieManager; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->transportBuilderMock = $this->_objectManager->get(TransportBuilderMock::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->customerRegistry = $this->_objectManager->get(CustomerRegistry::class); + $this->cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $this->urlBuilder = $this->_objectManager->get(UrlInterface::class); + } + + /** + * Tests that without form key user account won't be created + * and user will be redirected on account creation page again. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @return void + */ + public function testNoFormKeyCreatePostAction(): void + { + $this->fillRequestWithAccountData('test1@email.com'); + $this->getRequest()->setPostValue('form_key', null); + $this->dispatch('customer/account/createPost'); + + $this->assertCustomerNotExists('test1@email.com'); + $this->assertRedirect($this->stringEndsWith('customer/account/create/')); + $this->assertSessionMessages( + $this->stringContains((string)__('Invalid Form Key. Please refresh the page.')), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture current_website customer/create_account/confirm 0 + * @magentoConfigFixture current_store customer/create_account/default_group 1 + * @magentoConfigFixture current_store customer/create_account/generate_human_friendly_id 0 + * + * @return void + */ + public function testNoConfirmCreatePostAction(): void + { + $this->fillRequestWithAccountData('test1@email.com'); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringEndsWith('customer/account/')); + $this->assertSessionMessages( + $this->containsEqual( + (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()) + ), + MessageInterface::TYPE_SUCCESS + ); + $customer = $this->customerRegistry->retrieveByEmail('test1@email.com'); + //Assert customer group + $this->assertEquals(1, $customer->getDataModel()->getGroupId()); + //Assert customer increment id generation + $this->assertNull($customer->getData('increment_id')); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture current_website customer/create_account/confirm 0 + * @magentoConfigFixture current_store customer/create_account/default_group 2 + * @magentoConfigFixture current_store customer/create_account/generate_human_friendly_id 1 + * @return void + */ + public function testCreatePostWithCustomConfiguration(): void + { + $this->fillRequestWithAccountData('test@email.com'); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringEndsWith('customer/account/')); + $this->assertSessionMessages( + $this->containsEqual( + (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()) + ), + MessageInterface::TYPE_SUCCESS + ); + $customer = $this->customerRegistry->retrieveByEmail('test@email.com'); + //Assert customer group + $this->assertEquals(2, $customer->getDataModel()->getGroupId()); + //Assert customer increment id generation + $this->assertNotNull($customer->getData('increment_id')); + $this->assertMatchesRegularExpression('/\d{8}/', $customer->getData('increment_id')); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture current_website customer/create_account/confirm 1 + * + * @return void + */ + public function testWithConfirmCreatePostAction(): void + { + $email = 'test2@email.com'; + $this->fillRequestWithAccountData($email); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringContains('customer/account/index/')); + $message = 'You must confirm your account.' + . ' Please check your email for the confirmation link or <a href="%1">click here</a> for a new link.'; + $url = $this->urlBuilder->getUrl('customer/account/confirmation', ['_query' => ['email' => $email]]); + $this->assertSessionMessages( + $this->containsEqual((string)__($message, $url)), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testExistingEmailCreatePostAction(): void + { + $this->fillRequestWithAccountData('customer@example.com'); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringContains('customer/account/create/')); + $message = 'There is already an account with this email address.' + . ' If you are sure that it is your email address, <a href="%1">click here</a> ' + . 'to get your password and access your account.'; + $url = $this->urlBuilder->getUrl('customer/account/forgotpassword'); + $this->assertSessionMessages($this->containsEqual((string)__($message, $url)), MessageInterface::TYPE_ERROR); + } + + /** + * Register Customer with email confirmation. + * + * @magentoAppArea frontend + * @magentoConfigFixture current_website customer/create_account/confirm 1 + * + * @return void + */ + public function testRegisterCustomerWithEmailConfirmation(): void + { + $email = 'test_example@email.com'; + $this->fillRequestWithAccountData($email); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringContains('customer/account/index/')); + $message = 'You must confirm your account.' + . ' Please check your email for the confirmation link or <a href="%1">click here</a> for a new link.'; + $url = $this->urlBuilder->getUrl('customer/account/confirmation', ['_query' => ['email' => $email]]); + $this->assertSessionMessages($this->containsEqual((string)__($message, $url)), MessageInterface::TYPE_SUCCESS); + /** @var CustomerInterface $customer */ + $customer = $this->customerRepository->get($email); + $confirmation = $customer->getConfirmation(); + $sendMessage = $this->transportBuilderMock->getSentMessage(); + $this->assertNotNull($sendMessage); + $rawMessage = $sendMessage->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + (string)__( + 'You must confirm your %customer_email email before you can sign in (link is only valid once):', + ['customer_email' => $email] + ), + $rawMessage + ); + $this->assertStringContainsString( + sprintf('customer/account/confirm/?id=%s&key=%s', $customer->getId(), $confirmation), + $rawMessage + ); + $this->resetRequest(); + $this->getRequest() + ->setParam('id', $customer->getId()) + ->setParam('key', $confirmation); + $this->dispatch('customer/account/confirm'); + $this->assertRedirect($this->stringContains('customer/account/index/')); + $this->assertSessionMessages( + $this->containsEqual( + (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()) + ), + MessageInterface::TYPE_SUCCESS + ); + $this->assertEmpty($this->customerRepository->get($email)->getConfirmation()); + } + + /** + * Fills request with customer data. + * + * @param string $email + * @return void + */ + private function fillRequestWithAccountData(string $email): void + { + $this->getRequest() + ->setMethod(HttpRequest::METHOD_POST) + ->setParam(CustomerInterface::FIRSTNAME, 'firstname1') + ->setParam(CustomerInterface::LASTNAME, 'lastname1') + ->setParam(CustomerInterface::EMAIL, $email) + ->setParam('password', '_Password1') + ->setParam('password_confirmation', '_Password1') + ->setParam('telephone', '5123334444') + ->setParam('street', ['1234 fake street', '']) + ->setParam('city', 'Austin') + ->setParam('postcode', '78701') + ->setParam('country_id', 'US') + ->setParam('default_billing', '1') + ->setParam('default_shipping', '1') + ->setParam('is_subscribed', '0') + ->setPostValue('create_address', true); + } + + /** + * Asserts that customer does not exists. + * + * @param string $email + * @return void + */ + private function assertCustomerNotExists(string $email): void + { + $this->expectException(NoSuchEntityException::class); + $this->expectExceptionMessage( + (string)__( + 'No such entity with %fieldName = %fieldValue, %field2Name = %field2Value', + [ + 'fieldName' => 'email', + 'fieldValue' => $email, + 'field2Name' => 'websiteId', + 'field2Value' => 1 + ] + ) + ); + $this->assertNull($this->customerRepository->get($email)); + } + + /** + * Clears request. + * + * @return void + */ + protected function resetRequest(): void + { + parent::resetRequest(); + $this->cookieManager->deleteCookie(MessagePlugin::MESSAGES_COOKIES_NAME); + $this->_objectManager->removeSharedInstance(Http::class); + $this->_objectManager->removeSharedInstance(Request::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index 5527a39ce0507..6abbff18c645c 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -10,13 +10,10 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\CustomerRegistry; use Magento\Customer\Model\Session; -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Http; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Message\MessageInterface; -use Magento\Framework\Phrase; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\CookieManagerInterface; use Magento\Store\Model\StoreManager; @@ -220,83 +217,6 @@ public function testConfirmActionAlreadyActive() $this->getResponse()->getBody(); } - /** - * Tests that without form key user account won't be created - * and user will be redirected on account creation page again. - */ - public function testNoFormKeyCreatePostAction() - { - $this->fillRequestWithAccountData('test1@email.com'); - $this->getRequest()->setPostValue('form_key', null); - $this->dispatch('customer/account/createPost'); - - $this->assertNull($this->getCustomerByEmail('test1@email.com')); - $this->assertRedirect($this->stringEndsWith('customer/account/create/')); - $this->assertSessionMessages( - $this->equalTo([new Phrase('Invalid Form Key. Please refresh the page.')]), - MessageInterface::TYPE_ERROR - ); - } - - /** - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_disable.php - */ - public function testNoConfirmCreatePostAction() - { - $this->fillRequestWithAccountDataAndFormKey('test1@email.com'); - $this->dispatch('customer/account/createPost'); - $this->assertRedirect($this->stringEndsWith('customer/account/')); - $this->assertSessionMessages( - $this->equalTo(['Thank you for registering with Main Website Store.']), - MessageInterface::TYPE_SUCCESS - ); - } - - /** - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php - */ - public function testWithConfirmCreatePostAction() - { - $this->fillRequestWithAccountDataAndFormKey('test2@email.com'); - $this->dispatch('customer/account/createPost'); - $this->assertRedirect($this->stringContains('customer/account/index/')); - $this->assertSessionMessages( - $this->equalTo( - [ - 'You must confirm your account. Please check your email for the confirmation link or ' - . '<a href="http://localhost/index.php/customer/account/confirmation/' - . '?email=test2%40email.com">click here</a> for a new link.' - ] - ), - MessageInterface::TYPE_SUCCESS - ); - } - - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - */ - public function testExistingEmailCreatePostAction() - { - $this->fillRequestWithAccountDataAndFormKey('customer@example.com'); - $this->dispatch('customer/account/createPost'); - $this->assertRedirect($this->stringContains('customer/account/create/')); - $this->assertSessionMessages( - $this->equalTo( - [ - 'There is already an account with this email address. ' . - 'If you are sure that it is your email address, ' . - '<a href="http://localhost/index.php/customer/account/forgotpassword/">click here</a>' . - ' to get your password and access your account.', - ] - ), - MessageInterface::TYPE_ERROR - ); - } - /** * @magentoDataFixture Magento/Customer/_files/inactive_customer.php */ @@ -613,70 +533,6 @@ public function testWrongConfirmationEditPostAction() ); } - /** - * Register Customer with email confirmation. - * - * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php - * @return void - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException - * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException - */ - public function testRegisterCustomerWithEmailConfirmation(): void - { - $email = 'test_example@email.com'; - $this->fillRequestWithAccountDataAndFormKey($email); - $this->dispatch('customer/account/createPost'); - $this->assertRedirect($this->stringContains('customer/account/index/')); - $this->assertSessionMessages( - $this->equalTo( - [ - 'You must confirm your account. Please check your email for the confirmation link or ' - . '<a href="http://localhost/index.php/customer/account/confirmation/' - . '?email=test_example%40email.com">click here</a> for a new link.' - ] - ), - MessageInterface::TYPE_SUCCESS - ); - /** @var CustomerRepositoryInterface $customerRepository */ - $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); - /** @var CustomerInterface $customer */ - $customer = $customerRepository->get($email); - $confirmation = $customer->getConfirmation(); - $message = $this->transportBuilderMock->getSentMessage(); - $rawMessage = $message->getBody()->getParts()[0]->getRawContent(); - $messageConstraint = $this->logicalAnd( - new StringContains("You must confirm your {$email} email before you can sign in (link is only valid once"), - new StringContains("customer/account/confirm/?id={$customer->getId()}&key={$confirmation}") - ); - $this->assertThat($rawMessage, $messageConstraint); - - /** @var CookieManagerInterface $cookieManager */ - $cookieManager = $this->_objectManager->get(CookieManagerInterface::class); - $cookieManager->deleteCookie(MessagePlugin::MESSAGES_COOKIES_NAME); - - $this->_objectManager->removeSharedInstance(Http::class); - $this->_objectManager->removeSharedInstance(Request::class); - $this->_request = null; - - $this->getRequest() - ->setParam('id', $customer->getId()) - ->setParam('key', $confirmation); - $this->dispatch('customer/account/confirm'); - - /** @var StoreManager $store */ - $store = $this->_objectManager->get(StoreManagerInterface::class); - $name = $store->getStore()->getFrontendName(); - - $this->assertRedirect($this->stringContains('customer/account/index/')); - $this->assertSessionMessages( - $this->equalTo(["Thank you for registering with {$name}."]), - MessageInterface::TYPE_SUCCESS - ); - $this->assertEmpty($customerRepository->get($email)->getConfirmation()); - } - /** * Test that confirmation email address displays special characters correctly. * @@ -867,74 +723,6 @@ protected function resetRequest(): void parent::resetRequest(); } - /** - * @param string $email - * @return void - */ - private function fillRequestWithAccountData($email) - { - $this->getRequest() - ->setMethod('POST') - ->setParam('firstname', 'firstname1') - ->setParam('lastname', 'lastname1') - ->setParam('company', '') - ->setParam('email', $email) - ->setParam('password', '_Password1') - ->setParam('password_confirmation', '_Password1') - ->setParam('telephone', '5123334444') - ->setParam('street', ['1234 fake street', '']) - ->setParam('city', 'Austin') - ->setParam('region_id', 57) - ->setParam('region', '') - ->setParam('postcode', '78701') - ->setParam('country_id', 'US') - ->setParam('default_billing', '1') - ->setParam('default_shipping', '1') - ->setParam('is_subscribed', '0') - ->setPostValue('create_address', true); - } - - /** - * @param string $email - * @return void - */ - private function fillRequestWithAccountDataAndFormKey($email) - { - $this->fillRequestWithAccountData($email); - $formKey = $this->_objectManager->get(FormKey::class); - $this->getRequest()->setParam('form_key', $formKey->getFormKey()); - } - - /** - * Returns stored customer by email. - * - * @param string $email - * @return CustomerInterface - */ - private function getCustomerByEmail($email) - { - /** @var FilterBuilder $filterBuilder */ - $filterBuilder = $this->_objectManager->get(FilterBuilder::class); - $filters = [ - $filterBuilder->setField(CustomerInterface::EMAIL) - ->setValue($email) - ->create() - ]; - - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilters($filters) - ->create(); - - $customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); - $customers = $customerRepository->getList($searchCriteria) - ->getItems(); - - $customer = array_pop($customers); - - return $customer; - } - /** * Add new request info (request uri, path info, action name). * diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php index e12068ef62b21..bd2c26e449d72 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php @@ -13,9 +13,12 @@ use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Customer\Model\Customer; use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\EmailNotification; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory as TemplateCollectionFactory; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Api\SimpleDataObjectConverter; +use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; @@ -23,6 +26,7 @@ use Magento\Framework\Math\Random; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Validator\Exception; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; @@ -101,6 +105,16 @@ class CreateAccountTest extends TestCase */ private $encryptor; + /** + * @var MutableScopeConfigInterface + */ + private $mutableScopeConfig; + + /** + * @var TemplateCollectionFactory + */ + private $templateCollectionFactory; + /** * @inheritdoc */ @@ -117,9 +131,20 @@ protected function setUp(): void $this->customerModelFactory = $this->objectManager->get(CustomerFactory::class); $this->random = $this->objectManager->get(Random::class); $this->encryptor = $this->objectManager->get(EncryptorInterface::class); + $this->mutableScopeConfig = $this->objectManager->get(MutableScopeConfigInterface::class); + $this->templateCollectionFactory = $this->objectManager->get(TemplateCollectionFactory::class); parent::setUp(); } + /** + * @inheritdoc + */ + protected function tearDown(): void + { + parent::tearDown(); + $this->mutableScopeConfig->clean(); + } + /** * @dataProvider createInvalidAccountDataProvider * @param array $customerData @@ -220,6 +245,98 @@ public function createInvalidAccountDataProvider(): array ]; } + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer_welcome_email_template.php + * @return void + */ + public function testCreateAccountWithConfiguredWelcomeEmail(): void + { + $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_template'); + $this->setConfig([EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE => $emailTemplate,]); + $this->accountManagement->createAccount( + $this->populateCustomerEntity($this->defaultCustomerData), + '_Password1' + ); + $this->assertEmailData( + [ + 'name' => 'Owner', + 'email' => 'owner@example.com', + 'message' => 'Customer create account email template', + ] + ); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer_welcome_no_password_email_template.php + * @magentoConfigFixture current_store customer/create_account/email_identity support + * @return void + */ + public function testCreateAccountWithConfiguredWelcomeNoPasswordEmail(): void + { + $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_no_password_template'); + $this->setConfig([EmailNotification::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE => $emailTemplate,]); + $this->accountManagement->createAccount($this->populateCustomerEntity($this->defaultCustomerData)); + $this->assertEmailData( + [ + 'name' => 'CustomerSupport', + 'email' => 'support@example.com', + 'message' => 'Customer create account email no password template', + ] + ); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_email_template.php + * @magentoConfigFixture current_website customer/create_account/confirm 1 + * @magentoConfigFixture current_store customer/create_account/email_identity custom1 + * @return void + */ + public function testCreateAccountWithConfiguredConfirmationEmail(): void + { + $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_confirmation_template'); + $this->setConfig([EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE => $emailTemplate,]); + $this->accountManagement->createAccount( + $this->populateCustomerEntity($this->defaultCustomerData), + '_Password1' + ); + $this->assertEmailData( + [ + 'name' => 'Custom 1', + 'email' => 'custom1@example.com', + 'message' => 'Customer create account email confirmation template', + ] + ); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer_confirmed_email_template.php + * @magentoConfigFixture current_store customer/create_account/email_identity custom1 + * @magentoConfigFixture current_website customer/create_account/confirm 1 + * @return void + */ + public function testCreateAccountWithConfiguredConfirmedEmail(): void + { + $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_confirmed_template'); + $this->setConfig([EmailNotification::XML_PATH_CONFIRMED_EMAIL_TEMPLATE => $emailTemplate,]); + $this->accountManagement->createAccount( + $this->populateCustomerEntity($this->defaultCustomerData), + '_Password1' + ); + $customer = $this->customerRepository->get('customer@example.com'); + $this->accountManagement->activate($customer->getEmail(), $customer->getConfirmation()); + $this->assertEmailData( + [ + 'name' => 'Custom 1', + 'email' => 'custom1@example.com', + 'message' => 'Customer create account email confirmed template', + ] + ); + } + /** * Assert that when you create customer account via admin, link with "set password" is send to customer email. * @@ -589,4 +706,53 @@ private function assertCustomerData( ); } } + + /** + * Sets config data. + * + * @param array $configs + * @return void + */ + private function setConfig(array $configs): void + { + foreach ($configs as $path => $value) { + $this->mutableScopeConfig->setValue($path, $value, ScopeInterface::SCOPE_STORE, 'default'); + } + } + + /** + * Assert email data. + * + * @param array $expectedData + * @return void + */ + private function assertEmailData(array $expectedData): void + { + $message = $this->transportBuilderMock->getSentMessage(); + $this->assertNotNull($message); + $messageFrom = $message->getFrom(); + $this->assertNotNull($messageFrom); + $messageFrom = reset($messageFrom); + $this->assertEquals($expectedData['name'], $messageFrom->getName()); + $this->assertEquals($expectedData['email'], $messageFrom->getEmail()); + $this->assertStringContainsString( + $expectedData['message'], + $message->getBody()->getParts()[0]->getRawContent(), + 'Expected message wasn\'t found in email content.' + ); + } + + /** + * Returns email template id by template code. + * + * @param string $templateCode + * @return int + */ + private function getCustomTemplateId(string $templateCode): int + { + return (int)$this->templateCollectionFactory->create() + ->addFieldToFilter('template_code', $templateCode) + ->getFirstItem() + ->getId(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php index ac55f93bc9e4b..eb638eeb329aa 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php @@ -14,11 +14,16 @@ use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\CustomerRegistry; use Magento\Customer\Model\ResourceModel\Address; +use Magento\Customer\Model\Vat; +use Magento\Customer\Observer\AfterAddressSaveObserver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObjectFactory; use Magento\Framework\Exception\InputException; use Magento\TestFramework\Directory\Model\GetRegionIdByName; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface as PsrLogger; /** * Assert that address was created as expected or address create throws expected error. @@ -88,6 +93,11 @@ class CreateAddressTest extends TestCase */ private $createdAddressesIds = []; + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + /** * @inheritdoc */ @@ -101,6 +111,7 @@ protected function setUp(): void $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); $this->addressRegistry = $this->objectManager->get(AddressRegistry::class); $this->addressResource = $this->objectManager->get(Address::class); + $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class); parent::setUp(); } @@ -112,6 +123,7 @@ protected function tearDown(): void foreach ($this->createdAddressesIds as $createdAddressesId) { $this->addressRegistry->remove($createdAddressesId); } + $this->objectManager->removeSharedInstance(AfterAddressSaveObserver::class); parent::tearDown(); } @@ -326,6 +338,92 @@ public function createWrongAddressesDataProvider(): array ]; } + /** + * Assert that after address creation customer group is Group for Valid VAT ID - Domestic. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoConfigFixture current_store general/store_information/country_id AT + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_domestic_group 2 + * @return void + */ + public function testAddressCreatedWithGroupAssignByDomesticVatId(): void + { + $this->createVatMock(true, true); + $addressData = array_merge( + self::STATIC_CUSTOMER_ADDRESS_DATA, + [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT'] + ); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->createAddress((int)$customer->getId(), $addressData, false, true); + $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); + } + + /** + * Assert that after address creation customer group is Group for Valid VAT ID - Intra-Union. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoConfigFixture current_store general/store_information/country_id GR + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_intra_union_group 2 + * @return void + */ + public function testAddressCreatedWithGroupAssignByIntraUnionVatId(): void + { + $this->createVatMock(true, true); + $addressData = array_merge( + self::STATIC_CUSTOMER_ADDRESS_DATA, + [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT'] + ); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->createAddress((int)$customer->getId(), $addressData, false, true); + $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); + } + + /** + * Assert that after address creation customer group is Group for Invalid VAT ID. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_invalid_group 2 + * @return void + */ + public function testAddressCreatedWithGroupAssignByInvalidVatId(): void + { + $this->createVatMock(false, true); + $addressData = array_merge( + self::STATIC_CUSTOMER_ADDRESS_DATA, + [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT'] + ); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->createAddress((int)$customer->getId(), $addressData, false, true); + $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); + } + + /** + * Assert that after address creation customer group is Validation Error Group. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_error_group 2 + * @return void + */ + public function testAddressCreatedWithGroupAssignByVatIdWithError(): void + { + $this->createVatMock(false, false); + $addressData = array_merge( + self::STATIC_CUSTOMER_ADDRESS_DATA, + [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT'] + ); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->createAddress((int)$customer->getId(), $addressData, false, true); + $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); + } + /** * Create customer address with provided address data. * @@ -361,4 +459,49 @@ protected function createAddress( return $address; } + + /** + * Creates mock for vat id validation. + * + * @param bool $isValid + * @param bool $isRequestSuccess + * @return void + */ + private function createVatMock(bool $isValid = false, bool $isRequestSuccess = false): void + { + $gatewayResponse = $this->dataObjectFactory->create( + [ + 'data' => [ + 'is_valid' => $isValid, + 'request_date' => '', + 'request_identifier' => '123123123', + 'request_success' => $isRequestSuccess, + 'request_message' => __(''), + ], + ] + ); + $customerVat = $this->getMockBuilder(Vat::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ScopeConfigInterface::class), + $this->objectManager->get(PsrLogger::class) + ] + ) + ->setMethods(['checkVatNumber']) + ->getMock(); + $customerVat->method('checkVatNumber')->willReturn($gatewayResponse); + $this->objectManager->removeSharedInstance(Vat::class); + $this->objectManager->addSharedInstance($customerVat, Vat::class); + } + + /** + * Returns customer group id by email. + * + * @param string $email + * @return int + */ + private function getCustomerGroupId(string $email): int + { + return (int)$this->customerRepository->get($email)->getGroupId(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php new file mode 100644 index 0000000000000..38b607230cbaf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TemplateInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var TemplateInterfaceFactory $templateFactory */ +$templateFactory = $objectManager->get(TemplateInterfaceFactory::class); +/** @var TemplateInterface $template */ +$template = $templateFactory->create(); + +$content = <<<HTML +{{template config_path="design/email/header_template"}} +<p>{{trans "Customer create account email confirmation template"}}</p> +{{template config_path="design/email/footer_template"}} +HTML; + +$template->setTemplateCode('customer_create_account_email_confirmation_template') + ->setTemplateText($content) + ->setTemplateType(TemplateInterface::TYPE_HTML); +$templateResource->save($template); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_rollback.php new file mode 100644 index 0000000000000..07fee6e81fe47 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_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\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\ResourceModel\Template\Collection; +use Magento\Framework\Mail\TemplateInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var Collection $collection */ +$collection = $objectManager->get(CollectionFactory::class)->create(); +/** @var TemplateInterface $template */ +$template = $collection + ->addFieldToFilter('template_code', 'customer_create_account_email_confirmation_template') + ->getFirstItem(); +if ($template->getId()) { + $templateResource->delete($template); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php new file mode 100644 index 0000000000000..859cae92dbd27 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TemplateInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var TemplateInterfaceFactory $templateFactory */ +$templateFactory = $objectManager->get(TemplateInterfaceFactory::class); +/** @var TemplateInterface $template */ +$template = $templateFactory->create(); + +$content = <<<HTML +{{template config_path="design/email/header_template"}} +<p>{{trans "Customer create account email confirmed template"}}</p> +{{template config_path="design/email/footer_template"}} +HTML; + +$template->setTemplateCode('customer_create_account_email_confirmed_template') + ->setTemplateText($content) + ->setTemplateType(TemplateInterface::TYPE_HTML); +$templateResource->save($template); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_rollback.php new file mode 100644 index 0000000000000..a4e03038d45bd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_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\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\ResourceModel\Template\Collection; +use Magento\Framework\Mail\TemplateInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var Collection $collection */ +$collection = $objectManager->get(CollectionFactory::class)->create(); +/** @var TemplateInterface $template */ +$template = $collection + ->addFieldToFilter('template_code', 'customer_create_account_email_confirmed_template') + ->getFirstItem(); +if ($template->getId()) { + $templateResource->delete($template); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php new file mode 100644 index 0000000000000..6cc273dbe235a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TemplateInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var TemplateInterfaceFactory $templateFactory */ +$templateFactory = $objectManager->get(TemplateInterfaceFactory::class); +/** @var TemplateInterface $template */ +$template = $templateFactory->create(); + +$content = <<<HTML +{{template config_path="design/email/header_template"}} +<p>{{trans "Customer create account email template"}}</p> +{{template config_path="design/email/footer_template"}} +HTML; + +$template->setTemplateCode('customer_create_account_email_template') + ->setTemplateText($content) + ->setTemplateType(TemplateInterface::TYPE_HTML); +$templateResource->save($template); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_rollback.php new file mode 100644 index 0000000000000..6bef9822d3e9a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_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\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\ResourceModel\Template\Collection; +use Magento\Framework\Mail\TemplateInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var Collection $collection */ +$collection = $objectManager->get(CollectionFactory::class)->create(); +/** @var TemplateInterface $template */ +$template = $collection->addFieldToFilter('template_code', 'customer_create_account_email_template')->getFirstItem(); +if ($template->getId()) { + $templateResource->delete($template); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php new file mode 100644 index 0000000000000..a936bb9a4eb02 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TemplateInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var TemplateInterfaceFactory $templateFactory */ +$templateFactory = $objectManager->get(TemplateInterfaceFactory::class); +/** @var TemplateInterface $template */ +$template = $templateFactory->create(); + +$content = <<<HTML +{{template config_path="design/email/header_template"}} +<p>{{trans "Customer create account email no password template"}}</p> +{{template config_path="design/email/footer_template"}} +HTML; + +$template->setTemplateCode('customer_create_account_email_no_password_template') + ->setTemplateText($content) + ->setTemplateType(TemplateInterface::TYPE_HTML); +$templateResource->save($template); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_rollback.php new file mode 100644 index 0000000000000..4e14b4293cbb5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_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\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\ResourceModel\Template\Collection; +use Magento\Framework\Mail\TemplateInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var Collection $collection */ +$collection = $objectManager->get(CollectionFactory::class)->create(); +/** @var TemplateInterface $template */ +$template = $collection + ->addFieldToFilter('template_code', 'customer_create_account_email_no_password_template') + ->getFirstItem(); +if ($template->getId()) { + $templateResource->delete($template); +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php new file mode 100644 index 0000000000000..ca1f309c5cc9b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Block\Form; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Test for remember me checkbox on create customer account page + * + * @see \Magento\Persistent\Block\Form\Remember + * @magentoAppArea frontend + */ +class RememberTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Remember */ + private $block; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Remember::class) + ->setTemplate('Magento_Persistent::remember_me.phtml'); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_default 0 + * + * @return void + */ + public function testRememberMeEnabled(): void + { + $this->assertFalse($this->block->isRememberMeChecked()); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + '//input[@name="persistent_remember_me"]/following-sibling::label/span[contains(text(), "%s")]', + __('Remember Me') + ), + $this->block->toHtml() + ), + 'Remember Me checkbox wasn\'t found.' + ); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_default 1 + * + * @return void + */ + public function testRememberMeAndRememberDefaultEnabled(): void + { + $this->assertTrue($this->block->isRememberMeChecked()); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + '//input[@name="persistent_remember_me"]/following-sibling::label/span[contains(text(), "%s")]', + __('Remember Me') + ), + $this->block->toHtml() + ), + 'Remember Me checkbox wasn\'t found or not checked by default.' + ); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 0 + * + * @return void + */ + public function testPersistentDisabled(): void + { + $this->assertEmpty($this->block->toHtml()); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 0 + * + * @return void + */ + public function testRememberMeDisabled(): void + { + $this->assertEmpty($this->block->toHtml()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php new file mode 100644 index 0000000000000..16ce015d89ecd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Helper; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for persistent session helper + * + * @see \Magento\Persistent\Helper\Session + * @magentoDbIsolation enabled + * @magentoAppArea frontend + */ +class SessionTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Session */ + private $helper; + + /** @var SessionFactory */ + private $sessionFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(Session::class); + $this->sessionFactory = $this->objectManager->get(SessionFactory::class); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testPersistentEnabled(): void + { + $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1)); + $this->assertTrue($this->helper->isPersistent()); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent.php + * @magentoConfigFixture current_store persistent/options/enabled 0 + * + * @return void + */ + public function testPersistentDisabled(): void + { + $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1)); + $this->assertFalse($this->helper->isPersistent()); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testCustomerWithoutPersistent(): void + { + $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1)); + $this->assertFalse($this->helper->isPersistent()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php new file mode 100644 index 0000000000000..803e1502e3ad9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model\Checkout; + +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Checkout\Model\DefaultConfigProvider; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Helper\Session as PersistentSessionHelper; +use Magento\Persistent\Model\Session as PersistentSession; +use Magento\Persistent\Model\SessionFactory as PersistentSessionFactory; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Test for checkout config provider plugin + * + * @see \Magento\Persistent\Model\Checkout\ConfigProviderPlugin + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ConfigProviderPluginTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var DefaultConfigProvider */ + private $configProvider; + + /** @var CustomerSession */ + private $customerSession; + + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var QuoteIdMask */ + private $quoteIdMask; + + /** @var PersistentSessionHelper */ + private $persistentSessionHelper; + + /** @var PersistentSession */ + private $persistentSession; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->configProvider = $this->objectManager->get(DefaultConfigProvider::class); + $this->customerSession = $this->objectManager->get(CustomerSession::class); + $this->checkoutSession = $this->objectManager->get(CheckoutSession::class); + $this->quoteIdMask = $this->objectManager->get(QuoteIdMaskFactory::class)->create(); + $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class); + $this->persistentSession = $this->objectManager->get(PersistentSessionFactory::class)->create(); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->customerSession->setCustomerId(null); + $this->checkoutSession->clearQuote(); + $this->checkoutSession->setCustomerData(null); + $this->persistentSessionHelper->setSession(null); + + parent::tearDown(); + } + + /** + * @return void + */ + public function testPluginIsRegistered(): void + { + $pluginInfo = $this->objectManager->get(PluginList::class)->get(DefaultConfigProvider::class); + $this->assertSame(ConfigProviderPlugin::class, $pluginInfo['mask_quote_id_substitutor']['instance']); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testWithNotLoggedCustomer(): void + { + $session = $this->persistentSession->loadByCustomerId(1); + $this->persistentSessionHelper->setSession($session); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $result = $this->configProvider->getConfig(); + $this->assertEquals( + $this->quoteIdMask->load($quote->getId(), 'quote_id')->getMaskedId(), + $result['quoteData']['entity_id'] + ); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testWithLoggedCustomer(): void + { + $this->customerSession->setCustomerId(1); + $session = $this->persistentSession->loadByCustomerId(1); + $this->persistentSessionHelper->setSession($session); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $result = $this->configProvider->getConfig(); + $this->assertEquals($quote->getId(), $result['quoteData']['entity_id']); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * @magentoConfigFixture current_store persistent/options/enabled 0 + * + * @return void + */ + public function testPersistentDisabled(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $result = $this->configProvider->getConfig(); + $this->assertNull($result['quoteData']['entity_id']); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testWithoutPersistentSession(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $result = $this->configProvider->getConfig(); + $this->assertNull($result['quoteData']['entity_id']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php new file mode 100644 index 0000000000000..176224bad7a1f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for remember me checkbox on create customer account page. + * + * @see \Magento\Persistent\Model\CheckoutConfigProvider + */ +class CheckoutConfigProviderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CheckoutConfigProvider */ + private $model; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(CheckoutConfigProvider::class); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_default 1 + * + * @return void + */ + public function testRememberMeEnabled(): void + { + $expectedConfig = [ + 'persistenceConfig' => ['isRememberMeCheckboxVisible' => true, 'isRememberMeCheckboxChecked' => true], + ]; + $config = $this->model->getConfig(); + $this->assertEquals($expectedConfig, $config); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 0 + * @magentoConfigFixture current_store persistent/options/remember_default 0 + * + * @return void + */ + public function testRememberMeDisabled(): void + { + $expectedConfig = [ + 'persistenceConfig' => ['isRememberMeCheckboxVisible' => false, 'isRememberMeCheckboxChecked' => false], + ]; + $config = $this->model->getConfig(); + $this->assertEquals($expectedConfig, $config); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 0 + * @magentoConfigFixture current_store persistent/options/remember_default 0 + * + * @return void + */ + public function testPersistentDisabled(): void + { + $expectedConfig = [ + 'persistenceConfig' => ['isRememberMeCheckboxVisible' => false, 'isRememberMeCheckboxChecked' => false], + ]; + $config = $this->model->getConfig(); + $this->assertEquals($expectedConfig, $config); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php new file mode 100644 index 0000000000000..e11d47af3e814 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model; + +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Helper\Session as PersistentSessionHelper; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Test for persistent quote manager model + * + * @see \Magento\Persistent\Model\QuoteManager + * @magentoDbIsolation enabled + */ +class QuoteManagerTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var QuoteManager */ + private $model; + + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** @var PersistentSessionHelper */ + private $persistentSessionHelper; + + /** @var CartInterface */ + private $quote; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(QuoteManager::class); + $this->checkoutSession = $this->objectManager->get(CheckoutSession::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->checkoutSession->clearQuote(); + $this->checkoutSession->setCustomerData(null); + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/shopping_cart 1 + * + * @return void + */ + public function testPersistentShoppingCartEnabled(): void + { + $customerQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($customerQuote->getId()); + $this->model->setGuest(true); + $this->quote = $this->checkoutSession->getQuote(); + $this->assertNotEquals($customerQuote->getId(), $this->quote->getId()); + $this->assertFalse($this->model->isPersistent()); + $this->assertNull($this->quote->getCustomerId()); + $this->assertNull($this->quote->getCustomerEmail()); + $this->assertNull($this->quote->getCustomerFirstname()); + $this->assertNull($this->quote->getCustomerLastname()); + $this->assertEquals(GroupInterface::NOT_LOGGED_IN_ID, $this->quote->getCustomerGroupId()); + $this->assertEmpty($this->quote->getIsPersistent()); + $this->assertNull($this->persistentSessionHelper->getSession()->getId()); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/shopping_cart 0 + * + * @return void + */ + public function testPersistentShoppingCartDisabled(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $this->model->setGuest(true); + $this->assertNull($this->checkoutSession->getQuote()->getId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php index 35f2283494b1c..bd4d24211f1e3 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php @@ -10,16 +10,21 @@ use DateTime; use DateTimeZone; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Framework\Event; -use Magento\Framework\Event\Observer; +use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Persistent\Helper\Session as PersistentSessionHelper; use Magento\Persistent\Model\Session; use Magento\Persistent\Model\SessionFactory; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** + * Test for synchronize persistent session on login observer + * + * @see \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver + * @magentoAppArea frontend + * @magentoDbIsolation enabled * @magentoDataFixture Magento/Customer/_files/customer.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -28,87 +33,129 @@ class SynchronizePersistentOnLoginObserverTest extends TestCase /** * @var SynchronizePersistentOnLoginObserver */ - protected $_model; + private $model; /** * @var ObjectManagerInterface */ - protected $_objectManager; + private $objectManager; /** - * @var \Magento\Persistent\Helper\Session + * @var PersistentSessionHelper */ - protected $_persistentSession; + private $persistentSessionHelper; /** - * @var \Magento\Customer\Model\Session + * @var CustomerRepositoryInterface */ - protected $_customerSession; + private $customerRepository; /** - * @var CustomerInterface + * @var SessionFactory */ - private $customer; + private $persistentSessionFactory; + + /** + * @var CookieManagerInterface + */ + private $cookieManager; + + /** + * @var CustomerSession + */ + private $customerSession; /** * @inheritDoc */ protected function setUp(): void { - $this->_objectManager = Bootstrap::getObjectManager(); - $this->_persistentSession = $this->_objectManager->get(\Magento\Persistent\Helper\Session::class); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); - $this->_model = $this->_objectManager->create( - SynchronizePersistentOnLoginObserver::class, - [ - 'persistentSession' => $this->_persistentSession, - 'customerSession' => $this->_customerSession - ] - ); - /** @var CustomerRepositoryInterface $customerRepository */ - $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); - $this->customer = $customerRepository->getById(1); + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class); + $this->model = $this->objectManager->get(SynchronizePersistentOnLoginObserver::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->persistentSessionFactory = $this->objectManager->get(SessionFactory::class); + $this->cookieManager = $this->objectManager->get(CookieManagerInterface::class); + $this->customerSession = $this->objectManager->get(CustomerSession::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->persistentSessionHelper->setRememberMeChecked(null); + $this->customerSession->logout(); + + parent::tearDown(); } /** * Test that persistent session is created on customer login + * + * @return void */ public function testSynchronizePersistentOnLogin(): void { - $sessionModel = $this->_objectManager->create(Session::class); - $sessionModel->loadByCustomerId($this->customer->getId()); + $customer = $this->customerRepository->get('customer@example.com'); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->loadByCustomerId($customer->getId()); $this->assertNull($sessionModel->getCustomerId()); - $event = new Event(); - $observer = new Observer(['event' => $event]); - $event->setData('customer', $this->customer); - $this->_persistentSession->setRememberMeChecked(true); - $this->_model->execute($observer); - // check that persistent session has been stored for Customer - /** @var Session $sessionModel */ - $sessionModel = $this->_objectManager->create(Session::class); - $sessionModel->loadByCustomerId($this->customer->getId()); - $this->assertEquals($this->customer->getId(), $sessionModel->getCustomerId()); + $this->persistentSessionHelper->setRememberMeChecked(true); + $this->customerSession->loginById($customer->getId()); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->loadByCustomerId($customer->getId()); + $this->assertEquals($customer->getId(), $sessionModel->getCustomerId()); } /** * Test that expired persistent session is renewed on customer login + * + * @return void */ public function testExpiredPersistentSessionShouldBeRenewedOnLogin(): void { + $customer = $this->customerRepository->get('customer@example.com'); $lastUpdatedAt = (new DateTime('-1day'))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s'); - /** @var Session $sessionModel */ - $sessionModel = $this->_objectManager->create(SessionFactory::class)->create(); - $sessionModel->setCustomerId($this->customer->getId()); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->setCustomerId($customer->getId()); $sessionModel->setUpdatedAt($lastUpdatedAt); $sessionModel->save(); - $event = new Event(); - $observer = new Observer(['event' => $event]); - $event->setData('customer', $this->customer); - $this->_persistentSession->setRememberMeChecked(true); - $this->_model->execute($observer); - /** @var Session $sessionModel */ - $sessionModel = $this->_objectManager->create(Session::class); - $sessionModel->loadByCustomerId(1); + $this->persistentSessionHelper->setRememberMeChecked(true); + $this->customerSession->loginById($customer->getId()); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->loadByCustomerId($customer->getId()); $this->assertGreaterThan($lastUpdatedAt, $sessionModel->getUpdatedAt()); } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 0 + * + * @return void + */ + public function testDisabledPersistentSession(): void + { + $customer = $this->customerRepository->get('customer@example.com'); + $this->customerSession->loginById($customer->getId()); + $this->assertNull($this->cookieManager->getCookie(Session::COOKIE_NAME)); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/lifetime 0 + * + * @return void + */ + public function testDisabledPersistentSessionLifetime(): void + { + $customer = $this->customerRepository->get('customer@example.com'); + $this->customerSession->loginById($customer->getId()); + $session = $this->persistentSessionFactory->create()->setLoadExpired()->loadByCustomerId($customer->getId()); + $this->assertNull($session->getId()); + $this->assertNull($this->cookieManager->getCookie(Session::COOKIE_NAME)); + } } diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php index 2bf97fdb4953f..293f1d1890d92 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php @@ -3,54 +3,77 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Persistent\Observer; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** + * Test for synchronize persistent on logout observer + * + * @see \Magento\Persistent\Observer\SynchronizePersistentOnLogoutObserver * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoAppArea frontend + * @magentoDbIsolation enabled */ -class SynchronizePersistentOnLogoutObserverTest extends \PHPUnit\Framework\TestCase +class SynchronizePersistentOnLogoutObserverTest extends TestCase { - /** - * @var \Magento\Framework\ObjectManagerInterface - */ - protected $_objectManager; + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CustomerSession */ + private $customerSession; + + /** @var SessionFactory */ + private $sessionFactory; /** - * @var \Magento\Customer\Model\Session + * @inheritdoc */ - protected $_customerSession; - protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(CustomerSession::class); + $this->sessionFactory = $this->objectManager->get(SessionFactory::class); } /** * @magentoConfigFixture current_store persistent/options/enabled 1 * @magentoConfigFixture current_store persistent/options/logout_clear 1 - * @magentoAppArea frontend - * @magentoAppIsolation enabled + * + * @return void */ - public function testSynchronizePersistentOnLogout() + public function testSynchronizePersistentOnLogout(): void { - $this->_customerSession->loginById(1); - - // check that persistent session has been stored for Customer - /** @var \Magento\Persistent\Model\Session $sessionModel */ - $sessionModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Persistent\Model\Session::class - ); + $this->customerSession->loginById(1); + $sessionModel = $this->sessionFactory->create(); $sessionModel->loadByCookieKey(); $this->assertEquals(1, $sessionModel->getCustomerId()); - - $this->_customerSession->logout(); - - /** @var \Magento\Persistent\Model\Session $sessionModel */ - $sessionModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Persistent\Model\Session::class - ); + $this->customerSession->logout(); + $sessionModel = $this->sessionFactory->create(); $sessionModel->loadByCookieKey(); $this->assertNull($sessionModel->getCustomerId()); } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/logout_clear 0 + * + * @return void + */ + public function testSynchronizePersistentOnLogoutDisabled(): void + { + $this->customerSession->loginById(1); + $this->customerSession->logout(); + $sessionModel = $this->sessionFactory->create(); + $sessionModel->loadByCookieKey(); + $this->assertEquals(1, $sessionModel->getCustomerId()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_rollback.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_rollback.php new file mode 100644 index 0000000000000..581ddb35e3678 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_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/Customer/_files/customer_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php new file mode 100644 index 0000000000000..a2c68ad9b7f2a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_customer_without_address.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var SessionFactory $persistentSessionFactory */ +$persistentSessionFactory = $objectManager->get(SessionFactory::class); +$session = $persistentSessionFactory->create(); +$session->setCustomerId(1)->save(); +$session->setPersistentCookie(10000, ''); diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php new file mode 100644 index 0000000000000..252b3f4be7079 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var SessionFactory $sessionFactory */ +$sessionFactory = $objectManager->get(SessionFactory::class); +$sessionFactory->create()->deleteByCustomerId(1); + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_customer_without_address_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index f16986a3f2422..391be01b17f45 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -133,6 +133,6 @@ public function testChangeQuoteCustomerGroupIdForCustomerWithEnabledAutomaticGro ); $this->model->execute($eventObserver); - $this->assertEquals(1, $quote->getCustomer()->getGroupId()); + $this->assertEquals(2, $quote->getCustomer()->getGroupId()); } } diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php new file mode 100644 index 0000000000000..df5f5f8336303 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Block\Account; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks "My Product Reviews" link displaying in customer account dashboard + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ +class LinkTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Page */ + private $page; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->page = $this->objectManager->get(PageFactory::class)->create(); + } + + /** + * @return void + */ + public function testMyProductReviewsLink(): void + { + $this->preparePage(); + $block = $this->page->getLayout()->getBlock('customer-account-navigation-product-reviews-link'); + $this->assertNotFalse($block); + $html = $block->toHtml(); + $this->assertStringContainsString('/review/customer/', $html); + $this->assertEquals((string)__('My Product Reviews'), strip_tags($html)); + } + + /** + * @magentoConfigFixture current_store catalog/review/active 0 + * + * @return void + */ + public function testMyProductReviewsLinkDisabled(): void + { + $this->preparePage(); + $block = $this->page->getLayout()->getBlock('customer-account-navigation-product-reviews-link'); + $this->assertFalse($block); + } + + /** + * Prepare page before render + * + * @return void + */ + private function preparePage(): void + { + $this->page->addHandle([ + 'default', + 'customer_account', + ]); + $this->page->getLayout()->generateXml(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php new file mode 100644 index 0000000000000..24cb2fe76a6d4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Block\Customer; + +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Test for customer product reviews grid. + * + * @see \Magento\Review\Block\Customer\ListCustomer + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ListCustomerTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Session */ + private $customerSession; + + /** @var ListCustomer */ + private $block; + + /** @var CollectionFactory */ + private $collectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(Session::class); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(ListCustomer::class) + ->setTemplate('Magento_Review::customer/list.phtml'); + $this->collectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Review/_files/customer_review_with_rating.php + * + * @return void + */ + public function testCustomerProductReviewsGrid(): void + { + $this->customerSession->setCustomerId(1); + $review = $this->collectionFactory->create()->addCustomerFilter(1)->addReviewSummary()->getFirstItem(); + $this->assertNotNull($review->getReviewId()); + $blockHtml = $this->block->toHtml(); + $createdDate = $this->block->dateFormat($review->getReviewCreatedAt()); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//td[contains(@class, 'date') and contains(text(), '%s')]", $createdDate), + $blockHtml + ), + sprintf('Created date wasn\'t found or not equals to %s.', $createdDate) + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//td[contains(@class, 'item')]//a[contains(text(), '%s')]", $review->getName()), + $blockHtml + ), + 'Product name wasn\'t found.' + ); + $rating = $review->getSum() / $review->getCount(); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//td[contains(@class, 'summary')]//span[contains(text(), '%s%%')]", $rating), + $blockHtml + ), + sprintf('Rating wasn\'t found or not equals to %s%%.', $rating) + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//td[contains(@class, 'description') and contains(text(), '%s')]", $review->getDetail()), + $blockHtml + ), + 'Review description wasn\'t found.' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//td[contains(@class, 'actions')]//a[contains(@href, '%s')]/span[contains(text(), '%s')]", + $this->block->getReviewUrl($review), + __('See Details') + ), + $blockHtml + ), + sprintf('%s button wasn\'t found.', __('See Details')) + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testCustomerWithoutReviews(): void + { + $this->customerSession->setCustomerId(1); + $this->assertStringContainsString( + (string)__('You have submitted no reviews.'), + strip_tags($this->block->toHtml()) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php new file mode 100644 index 0000000000000..31a342ad8ac54 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Block\Customer; + +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Test for displaying customer product review block. + * + * @see \Magento\Review\Block\Customer\View + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ViewTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Session */ + private $customerSession; + + /** @var CollectionFactory */ + private $collectionFactory; + + /** @var View */ + private $block; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(Session::class); + $this->collectionFactory = $this->objectManager->get(CollectionFactory::class); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(View::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Review/_files/customer_review_with_rating.php + * + * @return void + */ + public function testCustomerProductReviewBlock(): void + { + $this->customerSession->setCustomerId(1); + $review = $this->collectionFactory->create()->addCustomerFilter(1)->getFirstItem(); + $this->assertNotNull($review->getReviewId()); + $blockHtml = $this->block->setReviewId($review->getReviewId())->toHtml(); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//div[contains(@class, 'product-info')]/h2[contains(text(), '%s')]", $review->getName()), + $blockHtml + ), + 'Product name wasn\'t found.' + ); + $ratings = $this->block->getRating(); + $this->assertCount(2, $ratings); + foreach ($ratings as $rating) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'rating-summary')]//span[contains(text(), '%s')]" + . "/../..//span[contains(text(), '%s%%')]", + $rating->getRatingCode(), + $rating->getPercent() + ), + $blockHtml + ), + sprintf('Rating %s was not found or not equals to %s.', $rating->getRatingCode(), $rating->getPercent()) + ); + } + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//div[contains(@class, 'review-title') and contains(text(), '%s')]", $review->getTitle()), + $blockHtml + ), + 'Review title wasn\'t found.' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//div[contains(@class, 'review-content') and contains(text(), '%s')]", $review->getDetail()), + $blockHtml + ), + 'Review description wasn\'t found.' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'review-date') and contains(text(), '%s')]/time[contains(text(), '%s')]", + __('Submitted on'), + $this->block->dateFormat($review->getCreatedAt()) + ), + $blockHtml + ), + 'Created date wasn\'t found.' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//a[contains(@href, '/review/customer/')]/span[contains(text(), '%s')]", + __('Back to My Reviews') + ), + $blockHtml + ), + sprintf('%s button wasn\'t found.', __('Back to My Reviews')) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php new file mode 100644 index 0000000000000..b26b71803848f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create\Items; + +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Block\Adminhtml\Order\Create\Items; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Checks order items grid + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class GridTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var LayoutInterface */ + private $layout; + + /** @var Grid */ + private $block; + + /** @var Quote */ + private $session; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->session = $this->objectManager->get(Quote::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(Grid::class); + $this->layout->createBlock(Items::class)->setChild('items_grid', $this->block); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testGetItems(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->session->setQuoteId($quote->getId()); + $items = $this->block->getItems(); + $this->assertCount(1, $items); + $this->assertEquals('simple2', reset($items)->getSku()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php new file mode 100644 index 0000000000000..291fda6e2494f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create\Sidebar; + +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Check sidebar shopping cart section block + * + * @see \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\Cart + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class CartTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Cart */ + private $block; + + /** @var Quote */ + private $session; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Cart::class); + $this->session = $this->objectManager->get(Quote::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->session->clearStorage(); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testGetItemCollection(): void + { + $this->session->setCustomerId(1); + $items = $this->block->getItemCollection(); + $this->assertCount(1, $items); + $this->assertEquals('simple2', reset($items)->getSku()); + } + + /** + * @return void + */ + public function testClearShoppingCartButton(): void + { + $confirmation = __('Are you sure you want to delete all items from shopping cart?'); + $button = $this->block->getChildBlock('empty_customer_cart_button'); + $this->assertEquals(sprintf("order.clearShoppingCart('%s')", $confirmation), $button->getOnclick()); + $this->assertEquals(__('Clear Shopping Cart'), $button->getLabel()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php new file mode 100644 index 0000000000000..1b5772cec66de --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Invoice\Create; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Model\OrderFactory; +use Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks invoiced items grid appearance + * + * @see \Magento\Sales\Block\Adminhtml\Order\Invoice\Create\Items + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class ItemsTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Items */ + private $block; + + /** @var OrderFactory */ + private $orderFactory; + + /** @var Registry */ + private $registry; + + /** @var CollectionFactory */ + private $invoiceCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Items::class); + $this->orderFactory = $this->objectManager->get(OrderFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->invoiceCollectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_invoice'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/invoice.php + * + * @return void + */ + public function testGetUpdateButtonHtml(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $invoice = $this->invoiceCollectionFactory->create()->setOrderFilter($order)->setPageSize(1)->getFirstItem(); + $this->registry->unregister('current_invoice'); + $this->registry->register('current_invoice', $invoice); + $this->block->toHtml(); + $button = $this->block->getChildBlock('update_button'); + $this->assertEquals((string)__('Update Qty\'s'), (string)$button->getLabel()); + $this->assertStringContainsString( + sprintf('sales/index/updateQty/order_id/%u/', (int)$order->getEntityId()), + $button->getOnClick() + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php new file mode 100644 index 0000000000000..a78c221cb5f84 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order; + +use Magento\Backend\Model\Search\AuthorizationMock; +use Magento\Framework\Authorization; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Checks order create block + * + * @see \Magento\Sales\Block\Adminhtml\Order\View + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class ViewTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var LayoutInterface */ + private $layout; + + /** @var Registry */ + private $registry; + + /** @var OrderFactory */ + private $orderFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->addSharedInstance( + $this->objectManager->get(AuthorizationMock::class), + Authorization::class + ); + $this->registry = $this->objectManager->get(Registry::class); + $this->orderFactory = $this->objectManager->get(OrderFactory::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('sales_order'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testInvoiceButton(): void + { + $this->registerOrder('100000001'); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + '//button[@id=\'order_invoice\']', + $this->layout->createBlock(View::class)->getButtonsHtml() + ) + ); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php + * + * @return void + */ + public function testInvoiceButtonIsNotVisible(): void + { + $this->registerOrder('100000001'); + $this->assertEmpty( + Xpath::getElementsCountForXpath( + '//button[@id=\'order_invoice\']', + $this->layout->createBlock(View::class)->getButtonsHtml() + ) + ); + } + + /** + * Register order + * + * @param OrderInterface $order + * @return void + */ + private function registerOrder(string $orderIncrementId): void + { + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + $this->registry->unregister('sales_order'); + $this->registry->register('sales_order', $order); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php new file mode 100644 index 0000000000000..b6aa44bac1c4d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php @@ -0,0 +1,301 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Create; + +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Class checks create order load block controller. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\LoadBlock + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class LoadBlockTest extends AbstractBackendController +{ + /** @var LayoutInterface */ + private $layout; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** @var Quote */ + private $session; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var array */ + private $quoteIdsToRemove; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->layout = $this->_objectManager->get(LayoutInterface::class); + $this->getQuoteByReservedOrderId = $this->_objectManager->get(GetQuoteByReservedOrderId::class); + $this->session = $this->_objectManager->get(Quote::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->quoteIdsToRemove[] = $this->session->getQuote()->getId(); + foreach ($this->quoteIdsToRemove as $quoteId) { + try { + $this->quoteRepository->delete($this->quoteRepository->get($quoteId)); + } catch (NoSuchEntityException $e) { + //do nothing + } + } + + $this->session->clearStorage(); + + parent::tearDown(); + } + + /** + * @dataProvider responseFlagsProvider + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @param bool $asJson + * @param bool $asJsVarname + * @return void + */ + public function testAddProductToOrderFromShoppingCart(bool $asJson, bool $asJsVarname): void + { + $oldQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $params = $this->hydrateParams([ + 'json' => $asJson, + 'as_js_varname' => $asJsVarname, + ]); + $post = $this->hydratePost([ + 'sidebar' => [ + 'add_cart_item' => [ + $oldQuote->getItemsCollection()->getFirstItem()->getId() => 1, + ], + ], + ]); + + $this->dispatchWitParams($params, $post); + + $this->checkHandles(explode(',', $params['block']), $asJson); + $this->checkQuotes($oldQuote, 'simple2'); + + if ($asJsVarname) { + $this->assertRedirect($this->stringContains('sales/order_create/showUpdateResult')); + } + } + + /** + * @return array + */ + public function responseFlagsProvider(): array + { + return [ + 'as_json' => [ + 'as_json' => true, + 'as_js_varname' => false, + ], + 'as_plain' => [ + 'as_json' => false, + 'as_js_varname' => true, + ], + ]; + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testRemoveProductFromShoppingCart(): void + { + $oldQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $post = $this->hydratePost([ + 'sidebar' => [ + 'remove' => [ + $oldQuote->getItemsCollection()->getFirstItem()->getId() => 'cart', + ], + ], + ]); + $params = $this->hydrateParams(); + + $this->dispatchWitParams($params, $post); + + $this->checkHandles(explode(',', $params['block'])); + $this->checkQuotes($oldQuote); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testClearShoppingCart(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $post = $this->hydratePost([ + 'sidebar' => [ + 'empty_customer_cart' => '1', + ], + ]); + $params = $this->hydrateParams(); + + $this->dispatchWitParams($params, $post); + + $this->checkHandles(explode(',', $params['block'])); + $this->assertEmpty($quote->getItemsCollection(false)->getItems()); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/inactive_quote_with_customer.php + * + * @return void + */ + public function testMoveFromOrderToShoppingCart(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_inactive_quote'); + $this->session->setQuoteId($quote->getId()); + $post = $this->hydratePost([ + 'update_items' => '1', + 'item' => [ + $quote->getItemsCollection()->getFirstItem()->getId() => [ + 'qty' => '1', + 'use_discount' => '1', + 'action' => 'cart', + ], + ], + ]); + $params = $this->hydrateParams(['blocks' => null]); + $this->dispatchWitParams($params, $post); + $customerCart = $this->quoteRepository->getForCustomer(1); + $cartItems = $customerCart->getItemsCollection(); + $this->assertCount(1, $cartItems->getItems()); + $this->assertEquals('taxable_product', $cartItems->getFirstItem()->getSku()); + $this->quoteIdsToRemove[] = $customerCart->getId(); + } + + /** + * Check customer quotes + * + * @param CartInterface $oldQuote + * @param string|null $expectedSku + * @return void + */ + private function checkQuotes(CartInterface $oldQuote, ?string $expectedSku = null): void + { + $newQuote = $this->session->getQuote(); + $oldQuoteItemCollection = $oldQuote->getItemsCollection(false); + $this->assertEmpty($oldQuoteItemCollection->getItems()); + $newQuoteItemsCollection = $newQuote->getItemsCollection(false); + + if ($expectedSku !== null) { + $this->assertNotNull($newQuoteItemsCollection->getItemByColumnValue('sku', $expectedSku)); + } else { + $this->assertEmpty($newQuoteItemsCollection->getItems()); + } + } + + /** + * Check that all required handles were applied + * + * @param array $blocks + * @param bool $asJson + * @return void + */ + private function checkHandles(array $blocks, bool $asJson = true): void + { + $handles = $this->layout->getUpdate()->getHandles(); + + if ($asJson) { + $this->assertContains('sales_order_create_load_block_message', $handles); + $this->assertContains('sales_order_create_load_block_json', $handles); + } else { + $this->assertContains('sales_order_create_load_block_plain', $handles); + } + + foreach ($blocks as $block) { + $this->assertContains( + 'sales_order_create_load_block_' . $block, + $handles + ); + } + } + + /** + * Fill post params array to proper state + * + * @param array $inputArray + * @return array + */ + private function hydratePost(array $inputArray = []): array + { + return array_merge( + [ + 'customer_id' => 1, + 'store_id' => $this->storeManager->getStore('default')->getId(), + 'sidebar' => [], + ], + $inputArray + ); + } + + /** + * Fill params array to proper state + * + * @param array $inputArray + * @return array + */ + private function hydrateParams(array $inputArray = []): array + { + return array_merge( + [ + 'json' => true, + 'block' => 'sidebar,items,shipping_method,billing_method,totals,giftmessage', + 'as_js_varname' => true, + ], + $inputArray + ); + } + + /** + * Dispatch request with params + * + * @param array $params + * @param array $postParams + * @return void + */ + private function dispatchWitParams(array $params, array $postParams): void + { + $this->getRequest()->setMethod(Http::METHOD_POST) + ->setPostValue($postParams) + ->setParams($params); + $this->dispatch('backend/sales/order_create/loadBlock'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php index 726ba697beb12..3c26a53424d81 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php @@ -7,39 +7,34 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; -use Magento\Framework\Api\SearchCriteria; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\App\Request\Http; use Magento\Sales\Api\Data\InvoiceInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\OrderRepository; +use Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\TestCase\AbstractBackendController; /** * Abstract backend invoice test. */ -class AbstractInvoiceControllerTest extends AbstractBackendController +abstract class AbstractInvoiceControllerTest extends AbstractBackendController { - /** - * @var TransportBuilderMock - */ + /** @var TransportBuilderMock */ protected $transportBuilder; - /** - * @var OrderRepository - */ - protected $orderRepository; + /** @var string */ + protected $resource = 'Magento_Sales::sales_invoice'; - /** - * @var FormKey - */ - protected $formKey; + /** @var OrderRepository */ + private $orderRepository; - /** - * @var string - */ - protected $resource = 'Magento_Sales::sales_invoice'; + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var CollectionFactory */ + private $invoiceCollectionFactory; /** * @inheritdoc @@ -47,46 +42,71 @@ class AbstractInvoiceControllerTest extends AbstractBackendController protected function setUp(): void { parent::setUp(); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); $this->orderRepository = $this->_objectManager->get(OrderRepository::class); - $this->formKey = $this->_objectManager->get(FormKey::class); + $this->searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $this->invoiceCollectionFactory = $this->_objectManager->get(CollectionFactory::class); } /** + * Retrieve order + * * @param string $incrementalId * @return OrderInterface|null */ - protected function getOrder(string $incrementalId) + protected function getOrder(string $incrementalId): ?OrderInterface { - /** @var SearchCriteria $searchCriteria */ - $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) - ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + $searchCriteria = $this->searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) ->create(); - $orders = $this->orderRepository->getList($searchCriteria)->getItems(); - /** @var OrderInterface $order */ - $order = reset($orders); - return $order; + return reset($orders); } /** - * @param OrderInterface $order + * Get firs order invoice + * + * @param OrderInterface|int $order * @return InvoiceInterface */ - protected function getInvoiceByOrder(OrderInterface $order): InvoiceInterface + protected function getInvoiceByOrder($order): InvoiceInterface { - /** @var \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection $invoiceCollection */ - $invoiceCollection = $this->_objectManager->create( - \Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory::class - )->create(); + $invoiceCollection = $this->invoiceCollectionFactory->create(); - /** @var InvoiceInterface $invoice */ - $invoice = $invoiceCollection - ->setOrderFilter($order) - ->setPageSize(1) - ->getFirstItem(); + return $invoiceCollection->setOrderFilter($order)->setPageSize(1)->getFirstItem(); + } - return $invoice; + /** + * Prepare request + * + * @param array $postParams + * @param array $params + * @return void + */ + protected function prepareRequest(array $postParams = [], array $params = []): void + { + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($postParams); + } + + /** + * Normalize post parameters + * + * @param array $items + * @param string $commentText + * @param bool $doShipment + * @return array + */ + protected function hydratePost(array $items, string $commentText = '', $doShipment = false): array + { + return [ + 'invoice' => [ + 'items' => $items, + 'comment_text' => $commentText, + 'do_shipment' => $doShipment + ], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php index ee59a55acd9b1..c7711e8897696 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php @@ -30,10 +30,11 @@ class AddCommentTest extends AbstractInvoiceControllerTest public function testSendEmailOnAddInvoiceComment(): void { $comment = 'Test Invoice Comment'; - $order = $this->prepareRequest( - [ - 'comment' => ['comment' => $comment, 'is_customer_notified' => true], - ] + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + $this->prepareRequest( + ['comment' => ['comment' => $comment, 'is_customer_notified' => true]], + ['id' => $invoice->getEntityId()] ); $this->dispatch('backend/sales/order_invoice/addComment'); @@ -41,6 +42,7 @@ public function testSendEmailOnAddInvoiceComment(): void $this->assertStringContainsString($comment, $html); $message = $this->transportBuilder->getSentMessage(); + $this->assertNotNull($message); $subject = __('Update to your %1 invoice', $order->getStore()->getFrontendName())->render(); $messageConstraint = $this->logicalAnd( new StringContains($order->getCustomerName()), @@ -55,7 +57,8 @@ public function testSendEmailOnAddInvoiceComment(): void ); $this->assertEquals($message->getSubject(), $subject); - $this->assertThat($message->getBody()->getParts()[0]->getRawContent(), $messageConstraint); + $bodyParts = $message->getBody()->getParts(); + $this->assertThat(reset($bodyParts)->getRawContent(), $messageConstraint); } /** @@ -63,7 +66,7 @@ public function testSendEmailOnAddInvoiceComment(): void */ public function testAclHasAccess() { - $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + $this->prepareRequest(); parent::testAclHasAccess(); } @@ -73,31 +76,8 @@ public function testAclHasAccess() */ public function testAclNoAccess() { - $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + $this->prepareRequest(); parent::testAclNoAccess(); } - - /** - * @param array $params - * @return \Magento\Sales\Api\Data\OrderInterface|null - */ - private function prepareRequest(array $params = []) - { - $order = $this->getOrder('100000001'); - $invoice = $this->getInvoiceByOrder($order); - - $this->getRequest()->setMethod('POST'); - $this->getRequest()->setParams( - [ - 'id' => $invoice->getEntityId(), - 'form_key' => $this->formKey->getFormKey(), - ] - ); - - $data = $params ?? []; - $this->getRequest()->setPostValue($data); - - return $order; - } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewActionTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewActionTest.php new file mode 100644 index 0000000000000..c8444c827d2e4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewActionTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use Magento\Framework\App\Request\Http; +use Magento\Framework\Escaper; +use Magento\Framework\Message\MessageInterface; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for new invoice action + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Invoice\NewAction + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class NewActionTest extends AbstractBackendController +{ + /** @var OrderFactory */ + private $orderFactory; + + /** @var Escaper */ + private $escaper; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + $this->escaper = $this->_objectManager->get(Escaper::class); + } + + /** + * @return void + */ + public function testWithNoExistingOrder(): void + { + $this->dispatchWithOrderId(863521); + $expectedMessage = (string)__("The entity that was requested doesn't exist. Verify the entity and try again."); + $this->assertSessionMessages($this->containsEqual($this->escaper->escapeHtml($expectedMessage))); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php + * + * @return void + */ + public function testCanNotInvoice(): void + { + $expectedMessage = __('The order does not allow an invoice to be created.'); + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->dispatchWithOrderId((int)$order->getEntityId()); + $this->assertSessionMessages($this->containsEqual((string)$expectedMessage), MessageInterface::TYPE_ERROR); + } + + /** + * Dispatch request with order_id param + * + * @param int $orderId + * @return void + */ + private function dispatchWithOrderId(int $orderId): void + { + $this->getRequest()->setMethod(Http::METHOD_GET) + ->setParams(['order_id' => $orderId]); + $this->dispatch('backend/sales/order_invoice/new'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php index a09e656b90726..13003e40dc0a3 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php @@ -7,38 +7,53 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; +use Magento\Framework\Escaper; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Model\Order; use PHPUnit\Framework\Constraint\StringContains; /** - * Class tests invoice creation in backend. + * Class tests invoice creation in admin panel. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Invoice\Save * * @magentoDbIsolation enabled * @magentoAppArea adminhtml - * @magentoDataFixture Magento/Sales/_files/order.php */ class SaveTest extends AbstractInvoiceControllerTest { + /** @var string */ + protected $uri = 'backend/sales/order_invoice/save'; + + /** @var Escaper */ + private $escaper; + /** - * @var string + * @inheritdoc */ - protected $uri = 'backend/sales/order_invoice/save'; + protected function setUp(): void + { + parent::setUp(); + + $this->escaper = $this->_objectManager->get(Escaper::class); + } /** + * @magentoDataFixture Magento/Sales/_files/order.php + * * @return void */ public function testSendEmailOnInvoiceSave(): void { - $order = $this->prepareRequest(); + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $post = $this->hydratePost([$itemId => 2]); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); $this->dispatch('backend/sales/order_invoice/save'); - - $this->assertSessionMessages( - $this->equalTo([(string)__('The invoice has been created.')]), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); - $invoice = $this->getInvoiceByOrder($order); + $this->checkSuccess($invoice, 2); $message = $this->transportBuilder->getSentMessage(); + $this->assertNotNull($message); $subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render(); $messageConstraint = $this->logicalAnd( new StringContains($invoice->getBillingAddress()->getName()), @@ -49,9 +64,113 @@ public function testSendEmailOnInvoiceSave(): void "Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}" ) ); - $this->assertEquals($message->getSubject(), $subject); - $this->assertThat($message->getBody()->getParts()[0]->getRawContent(), $messageConstraint); + $bodyParts = $message->getBody()->getParts(); + $this->assertThat(reset($bodyParts)->getRawContent(), $messageConstraint); + } + + /** + * @magentoConfigFixture current_store sales_email/invoice/enabled 0 + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testSendEmailOnInvoiceSaveWithDisabledConfig(): void + { + $order = $this->getOrder('100000001'); + $post = $this->hydratePost([$order->getItemsCollection()->getFirstItem()->getId() => 2]); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->checkSuccess($this->getInvoiceByOrder($order), 2); + $this->assertNull($this->transportBuilder->getSentMessage()); + } + + /** + * @dataProvider invoiceDataProvider + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @param int $invoicedItemsQty + * @param string $commentMessage + * @param bool $doShipment + * @return void + */ + public function testSuccessfulInvoice( + int $invoicedItemsQty, + string $commentMessage = '', + bool $doShipment = false + ): void { + $order = $this->getOrder('100000001'); + $post = $this->hydratePost( + [$order->getItemsCollection()->getFirstItem()->getId() => $invoicedItemsQty], + $commentMessage, + $doShipment + ); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->checkSuccess($this->getInvoiceByOrder($order), $invoicedItemsQty, $commentMessage, $doShipment); + } + + /** + * @return array + */ + public function invoiceDataProvider(): array + { + return [ + 'with_comment_message' => [ + 'invoiced_items_qty' => 2, + 'comment_message' => 'test comment message', + ], + 'partial_invoice' => [ + 'invoiced_items_qty' => 1, + ], + 'with_do_shipment' => [ + 'invoiced_items_qty' => 2, + 'comment_message' => '', + 'do_shipment' => true, + ], + ]; + } + + /** + * @return void + */ + public function testWitNoExistingOrder(): void + { + $expectedMessage = (string)__('The order no longer exists.'); + $this->prepareRequest(['order_id' => 899989]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->assertErrorResponse($expectedMessage); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php + * + * @return void + */ + public function testCanNotInvoiceOrder(): void + { + $expectedMessage = (string)__('The order does not allow an invoice to be created.'); + $order = $this->getOrder('100000001'); + $this->prepareRequest([], ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->assertErrorResponse($expectedMessage); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testInvoiceWithoutQty(): void + { + $expectedMessage = (string)__('The invoice can\'t be created without products. Add products and try again.'); + $order = $this->getOrder('100000001'); + $post = $this->hydratePost([$order->getItemsCollection()->getFirstItem()->getId() => '0']); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->assertErrorResponse($this->escaper->escapeHtml($expectedMessage)); } /** @@ -77,11 +196,14 @@ public function testAclNoAccess() /** * Checks that order protect code is not changing after invoice submitting * + * @magentoDataFixture Magento/Sales/_files/order.php + * * @return void */ public function testOrderProtectCodePreserveAfterInvoiceSave(): void { - $order = $this->prepareRequest(); + $order = $this->getOrder('100000001'); + $this->prepareRequest([], ['order_id' => $order->getEntityId()]); $protectCode = $order->getProtectCode(); $this->dispatch($this->uri); $invoicedOrder = $this->getOrder('100000001'); @@ -90,23 +212,46 @@ public function testOrderProtectCodePreserveAfterInvoiceSave(): void } /** - * @param array $params - * @return \Magento\Sales\Api\Data\OrderInterface|null + * Check error response + * + * @param string $expectedMessage + * @return void */ - private function prepareRequest(array $params = []) + private function assertErrorResponse(string $expectedMessage): void { - $order = $this->getOrder('100000001'); - $this->getRequest()->setMethod('POST'); - $this->getRequest()->setParams( - [ - 'order_id' => $order->getEntityId(), - 'form_key' => $this->formKey->getFormKey(), - ] - ); + $this->assertRedirect($this->stringContains('sales/order_invoice/new')); + $this->assertSessionMessages($this->containsEqual($expectedMessage)); + } - $data = $params ?? []; - $this->getRequest()->setPostValue($data); + /** + * Check that invoice was successfully created + * + * @param InvoiceInterface $invoice + * @param int $invoicedItemsQty + * @param string|null $commentMessage + * @param bool $doShipment + * @return void + */ + private function checkSuccess( + InvoiceInterface $invoice, + int $invoicedItemsQty, + ?string $commentMessage = null, + bool $doShipment = false + ): void { + $message = $doShipment ? 'You created the invoice and shipment.' : 'The invoice has been created.'; + $expectedState = $doShipment ? Order::STATE_COMPLETE : Order::STATE_PROCESSING; + $this->assertNotNull($invoice->getEntityId()); + $this->assertEquals($invoicedItemsQty, (int)$invoice->getTotalQty()); + $order = $invoice->getOrder(); + $this->assertEquals($expectedState, $order->getState()); - return $order; + if ($commentMessage) { + $this->assertEquals($commentMessage, $invoice->getCustomerNote()); + } + + $this->assertRedirect( + $this->stringContains(sprintf('sales/order/view/order_id/%u', (int)$order->getEntityId())) + ); + $this->assertSessionMessages($this->containsEqual((string)__($message))); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php new file mode 100644 index 0000000000000..5eb554ef937d5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use Magento\Backend\Model\Session; +use Magento\Framework\App\Request\Http; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for invoice start action + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Invoice\Start + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class StartTest extends AbstractBackendController +{ + /** @var OrderFactory */ + private $orderFactory; + + /** @var Session */ + private $session; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + $this->session = $this->_objectManager->get(Session::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->session->getInvoiceItemQtys(true); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testExecute(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->session->setInvoiceItemQtys('test'); + $this->getRequest()->setMethod(Http::METHOD_GET)->setParams(['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/start'); + $this->assertRedirect($this->stringContains('sales/order_invoice/new')); + $this->assertNull($this->session->getInvoiceItemQtys()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php new file mode 100644 index 0000000000000..2b91c5d04fd6f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\TestFramework\Helper\Xpath; + +/** + * Class tests invoice items qty update. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ +class UpdateQtyTest extends AbstractInvoiceControllerTest +{ + /** @var SerializerInterface */ + private $json; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->json = $this->_objectManager->get(SerializerInterface::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testSuccess(): void + { + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $qtyToInvoice = 1; + $invoicedItemsXpath = sprintf( + "//input[contains(@class, 'qty-input') and @name='invoice[items][%u]' and @value='%u']", + $itemId, + $qtyToInvoice + ); + $post = $this->hydratePost([$itemId => $qtyToInvoice]); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/updateQty'); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($invoicedItemsXpath, $this->getResponse()->getContent()) + ); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php + * + * @return void + */ + public function testCanNotInvoice(): void + { + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $post = $this->hydratePost([$itemId => '1']); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/updateQty'); + $this->assertErrorResponse('The order does not allow an invoice to be created.'); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testWithoutQty(): void + { + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $post = $this->hydratePost([$itemId => '0']); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/updateQty'); + $this->assertErrorResponse( + 'The invoice can\'t be created without products. Add products and try again.' + ); + } + + /** + * @return void + */ + public function testWithNoExistingOrderId(): void + { + $post = $this->hydratePost([ + 'invoice' => [ + 'items' => [ + '1' => '3', + ], + ], + ]); + $this->prepareRequest($post, ['order_id' => 6543265]); + $this->dispatch('backend/sales/order_invoice/updateQty'); + $this->assertErrorResponse('The order no longer exists.'); + } + + /** + * Check error response + * + * @param string $expectedMessage + * @return void + */ + private function assertErrorResponse(string $expectedMessage): void + { + $expectedResponse = [ + 'error' => true, + 'message' => (string)__($expectedMessage), + ]; + $response = $this->getResponse()->getContent(); + $this->assertNotEmpty($response); + $this->assertEquals($expectedResponse, $this->json->unserialize($response)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php new file mode 100644 index 0000000000000..cffdda80cc897 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Guest; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Helper\Guest; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test for guest reorder controller. + * + * @see \Magento\Sales\Controller\Guest\Reorder + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ReorderTest extends AbstractController +{ + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var CookieManagerInterface */ + private $cookieManager; + + /** @var Session */ + private $customerSession; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->checkoutSession = $this->_objectManager->get(CheckoutSession::class); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $this->customerSession = $this->_objectManager->get(Session::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $createdQuoteId = $this->checkoutSession->getQuoteId(); + + if ($createdQuoteId !== null) { + try { + $this->quoteRepository->delete($this->quoteRepository->get($createdQuoteId)); + } catch (NoSuchEntityException $e) { + //already deleted + } + } + + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDbIsolation disabled + * + * @magentoDataFixture Magento/Sales/_files/order_by_guest_with_simple_product.php + * + * @return void + */ + public function testReorderSimpleProduct(): void + { + $orderIncrementId = 'test_order_1'; + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + $cookieValue = base64_encode($order->getProtectCode() . ':' . $orderIncrementId); + $this->cookieManager->setPublicCookie(Guest::COOKIE_NAME, $cookieValue); + $this->dispatchReorderRequest(); + $this->assertRedirect($this->stringContains('checkout/cart')); + $quoteId = $this->checkoutSession->getQuoteId(); + $this->assertNotNull($quoteId); + $quoteItemsCollection = $this->quoteRepository->get((int)$quoteId)->getItemsCollection(); + $this->assertCount(1, $quoteItemsCollection); + $this->assertEquals( + $order->getItemsCollection()->getFirstItem()->getSku(), + $quoteItemsCollection->getFirstItem()->getSku() + ); + } + + /** + * @return void + */ + public function testReorderWithoutParamsAndCookie(): void + { + $this->dispatchReorderRequest(); + $this->assertRedirect($this->stringContains('sales/guest/form')); + $this->assertSessionMessages( + $this->containsEqual((string)__('You entered incorrect data. Please try again.')), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testReorderGuestOrderByCustomer(): void + { + $this->customerSession->setCustomerId(1); + $this->dispatchReorderRequest(); + $this->assertRedirect($this->stringContains('sales/order/history')); + } + + /** + * Dispatch reorder request. + * + * @return void + */ + private function dispatchReorderRequest(): void + { + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->dispatch('sales/guest/reorder/'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php new file mode 100644 index 0000000000000..3b32e7238cc76 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Order; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session; +use Magento\Framework\Escaper; +use Magento\Framework\Message\MessageInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Core\Version\View; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test for reorder controller. + * + * @see \Magento\Sales\Controller\Order\Reorder + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ReorderTest extends AbstractController +{ + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var Session */ + private $customerSession; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CartInterface */ + private $quote; + + /** @var Escaper */ + private $escaper; + + /** + * @var View + */ + private $versionChecker; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->checkoutSession = $this->_objectManager->get(CheckoutSession::class); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->customerSession = $this->_objectManager->get(Session::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $this->escaper = $this->_objectManager->get(Escaper::class); + $this->versionChecker = $this->_objectManager->get(View::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/customer_order_with_taxable_product.php + * + * @return void + */ + public function testReorder(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('test_order_with_taxable_product'); + $this->customerSession->setCustomerId($order->getCustomerId()); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('checkout/cart')); + $this->quote = $this->checkoutSession->getQuote(); + $quoteItemsCollection = $this->quote->getItemsCollection(); + $this->assertCount(1, $quoteItemsCollection); + $this->assertEquals( + $order->getItemsCollection()->getFirstItem()->getSku(), + $quoteItemsCollection->getFirstItem()->getSku() + ); + } + + /** + * @magentoDataFixture Magento/Sales/_files/customer_order_with_simple_product.php + * + * @return void + */ + public function testReorderProductLowQty(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('55555555'); + $this->customerSession->setCustomerId($order->getCustomerId()); + $this->dispatchReorderRequest((int)$order->getId()); + $origMessage = (string)__('The requested qty is not available'); + $message = $this->escaper->escapeHtml( + __('Could not add the product with SKU "%1" to the shopping cart: %2', 'simple-1', $origMessage) + ); + $constraint = $this->logicalOr($this->containsEqual($origMessage), $this->containsEqual($message)); + $this->assertThat($this->getMessages(MessageInterface::TYPE_ERROR), $constraint); + $this->quote = $this->checkoutSession->getQuote(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Sales/_files/customer_order_with_two_items.php + * + * @return void + */ + public function testReorderByAnotherCustomer(): void + { + $this->customerSession->setCustomerId(1); + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->dispatchReorderRequest((int)$order->getId()); + + if ($this->versionChecker->isVersionUpdated()) { + $this->assertRedirect($this->stringContains('noroute')); + } else { + $this->assertRedirect($this->stringContains('sales/order/history')); + } + } + + /** + * Dispatch reorder request. + * + * @param null|int $orderId + * @return void + */ + private function dispatchReorderRequest(?int $orderId = null): void + { + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('sales/order/reorder/'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php new file mode 100644 index 0000000000000..5a21f551ff1a7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Helper; + +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for reorder helper. + * + * @see \Magento\Sales\Helper\Reorder + * @magentoDbIsolation enabled + */ +class ReorderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Reorder */ + private $helper; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var Session */ + private $customerSession; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(Reorder::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->customerSession = $this->objectManager->get(Session::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testCanReorderForGuest(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->assertTrue($this->helper->canReorder($order->getId())); + } + + /** + * @magentoDataFixture Magento/Sales/_files/customer_order_with_two_items.php + * + * @return void + */ + public function testCanReorderForLoggedCustomer(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->customerSession->setCustomerId($order->getCustomerId()); + $this->assertTrue($this->helper->canReorder($order->getId())); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Sales/_files/order_state_hold.php + * + * @return void + */ + public function testCanReorderHoldOrderForLoggedCustomer(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->customerSession->setCustomerId(1); + $this->assertFalse($this->helper->canReorder($order->getId())); + } + + /** + * @magentoConfigFixture current_store sales/reorder/allow 0 + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testCanReorderConfigDisabled(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->assertFalse($this->helper->canReorder($order->getId())); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.php new file mode 100644 index 0000000000000..ca102b0fabf89 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/customer_quote_ready_for_order.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CartManagementInterface $quoteManagement */ +$quoteManagement = $objectManager->get(CartManagementInterface::class); + +$quote = $quoteRepository->getActiveForCustomer(1); +$quoteManagement->placeOrder($quote->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_rollback.php new file mode 100644 index 0000000000000..46cabc2e3fd9b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_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\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderInterfaceFactory::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$order = $orderFactory->create()->loadByIncrementId('55555555'); +if ($order->getId()) { + $orderRepository->delete($order); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php new file mode 100644 index 0000000000000..59ec4182ac870 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_taxable_product_and_customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CartManagementInterface $quoteManagement */ +$quoteManagement = $objectManager->get(CartManagementInterface::class); +/** @var PaymentInterface $payment */ +$payment = $objectManager->get(PaymentInterface::class); +$payment->setMethod('checkmo'); + +$quote = $quoteRepository->getActiveForCustomer(1); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getShippingAddress()->collectShippingRates(); +$quoteRepository->save($quote); +$quoteManagement->placeOrder($quote->getId(), $payment); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product_rollback.php new file mode 100644 index 0000000000000..d42f6a1140286 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product_rollback.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\Registry; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderInterfaceFactory::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$order = $orderFactory->create()->loadByIncrementId('test_order_with_taxable_product'); +if ($order->getId()) { + $orderRepository->delete($order); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture( + 'Magento/Checkout/_files/quote_with_taxable_product_and_customer_rollback.php' +); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php new file mode 100644 index 0000000000000..c3bab9acca27b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address_saved.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CartManagementInterface $quoteManagement */ +$quoteManagement = $objectManager->get(CartManagementInterface::class); +/** @var PaymentInterface $payment */ +$payment = $objectManager->get(PaymentInterface::class); + +$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('test_order_1'); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getShippingAddress()->collectShippingRates(); +$quoteRepository->save($quote); +$payment->setMethod('checkmo'); +$quoteManagement->placeOrder($quote->getId(), $payment); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_rollback.php new file mode 100644 index 0000000000000..b4ec514d1311e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_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\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderInterfaceFactory::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$order = $orderFactory->create()->loadByIncrementId('test_order_1'); +if ($order->getId()) { + $orderRepository->delete($order); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address_saved_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php new file mode 100644 index 0000000000000..954c23498ec66 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCoupons; + +use Magento\Framework\App\ResourceConnection; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection as RuleCollection; +use Magento\SalesRule\Model\Rule; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test export coupon csv + * + * Verify export csv + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_with_coupon_list.php + */ +class ExportCouponsCsvTest extends AbstractBackendController +{ + /** + * @var string + */ + protected $uri = 'backend/sales_rule/promo_quote/exportCouponsCsv'; + + /** + * @var string + */ + protected $resource = 'Magento_SalesRule::quote'; + + /** + * @var Rule + */ + private $salesRule; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->resourceConnection = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $this->initSalesRule(); + } + + /** + * Prepare request + * + * @return void + */ + private function prepareRequest(): void + { + $couponList = $this->getCouponsIdList(); + if (count($couponList)) { + $this->getRequest()->setParams(['internal_ids' => $couponList[0]])->setMethod('POST'); + } + } + + /** + * Init current sales rule + * + * @return void + */ + private function initSalesRule(): void + { + /** @var RuleCollection $collection */ + $collection = Bootstrap::getObjectManager()->create(RuleCollection::class); + $collection->addFieldToFilter('name', 'Rule with coupon list'); + $this->salesRule = $collection->getFirstItem(); + } + + /** + * Retrieve id list of coupons + * + * @return array + */ + private function getCouponsIdList(): array + { + $select = $this->resourceConnection->getConnection() + ->select() + ->from($this->resourceConnection->getTableName('salesrule_coupon')) + ->columns(['coupon_id']) + ->where('rule_id=?', $this->salesRule->getId()); + + return $this->resourceConnection->getConnection()->fetchCol($select); + } + + /** + * Test export csv + * + * @return void + */ + public function testExportCsv(): void + { + $this->prepareRequest(); + $this->dispatch($this->uri); + $this->assertStringNotContainsString('404 Error', $this->getResponse()->getBody()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php new file mode 100644 index 0000000000000..d222b064a0d2c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCoupons; + +use Magento\Framework\App\ResourceConnection; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection as RuleCollection; +use Magento\SalesRule\Model\Rule; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test export coupon xml + * + * Verify export xml + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_with_coupon_list.php + */ +class ExportCouponsXmlTest extends AbstractBackendController +{ + /** + * @var string + */ + protected $uri = 'backend/sales_rule/promo_quote/exportCouponsXml'; + + /** + * @var string + */ + protected $resource = 'Magento_SalesRule::quote'; + + /** + * @var Rule + */ + private $salesRule; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->resourceConnection = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $this->initSalesRule(); + } + + /** + * Prepare request + * + * @return void + */ + private function prepareRequest(): void + { + $couponList = $this->getCouponsIdList(); + if (count($couponList)) { + $this->getRequest()->setParams(['internal_ids' => $couponList[0]])->setMethod('POST'); + } + } + + /** + * Init current sales rule + * + * @return void + */ + private function initSalesRule(): void + { + /** @var RuleCollection $collection */ + $collection = Bootstrap::getObjectManager()->create(RuleCollection::class); + $collection->addFieldToFilter('name', 'Rule with coupon list'); + $this->salesRule = $collection->getFirstItem(); + } + + /** + * Retrieve id list of coupons + * + * @return array + */ + private function getCouponsIdList(): array + { + $select = $this->resourceConnection->getConnection() + ->select() + ->from($this->resourceConnection->getTableName('salesrule_coupon')) + ->columns(['coupon_id']) + ->where('rule_id=?', $this->salesRule->getId()); + + return $this->resourceConnection->getConnection()->fetchCol($select); + } + + /** + * Test export xml + * + * @return void + */ + public function testExportCsv(): void + { + $this->prepareRequest(); + $this->dispatch($this->uri); + $this->assertStringNotContainsString('404 Error', $this->getResponse()->getBody()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php index 3151a76327397..547ce78500f49 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php @@ -4,24 +4,35 @@ * See COPYING.txt for license details. */ declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Directory\Model\Currency as ModelCurrency; +use Magento\Directory\Model\ResourceModel\Currency as ResourceCurrency; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$store = $objectManager->create(\Magento\Store\Model\Store::class); +$objectManager = Bootstrap::getObjectManager(); +$store = $objectManager->create(Store::class); $storeId = $store->load('fixture_second_store', 'code')->getId(); if ($storeId) { - $configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); + $configResource = $objectManager->get(Config::class); $configResource->deleteConfig( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_DEFAULT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORES, + ModelCurrency::XML_PATH_CURRENCY_DEFAULT, + ScopeInterface::SCOPE_STORES, $storeId ); $configResource->deleteConfig( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW, - \Magento\Store\Model\ScopeInterface::SCOPE_STORES, + ModelCurrency::XML_PATH_CURRENCY_ALLOW, + ScopeInterface::SCOPE_STORES, $storeId ); } Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store_rollback.php'); +$reflectionClass = new \ReflectionClass(ResourceCurrency::class); +$staticProperty = $reflectionClass->getProperty('_rateCache'); +$staticProperty->setAccessible(true); +$staticProperty->setValue(null); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php index 5d1758f578836..dd715ecc93b0d 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php @@ -116,6 +116,7 @@ public function childProductsDataProvider(): array ], 'expected_data' => [ [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 50], 'finalPrice' => ['amount' => 50], @@ -123,6 +124,7 @@ public function childProductsDataProvider(): array 'msrpPrice' => ['amount' => null], ], [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 58.55], 'finalPrice' => ['amount' => 58.55], @@ -130,6 +132,7 @@ public function childProductsDataProvider(): array 'msrpPrice' => ['amount' => null], ], [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 75], 'finalPrice' => ['amount' => 75], diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php index 9a95ed4fd462d..1cbdf6144d640 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php @@ -62,6 +62,23 @@ public function testLoadedProductAttributes() $this->assertEquals('Short description', $productOnWishlist->getData('short_description')); } + /** + * Tests collection load. + * Tests collection load method when product salable filter flag is setted to true + * and few products are present. + * + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * @magentoDbIsolation disabled + */ + public function testLoadWhenFewProductsPresent() + { + $this->itemCollection->setSalableFilter(true); + $this->itemCollection->addCustomerIdFilter(1); + $this->itemCollection->load(); + $this->assertCount(1, $this->itemCollection->getItems()); + } + /** * @param array $attributes */ diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js index f46ff6b30abbe..c0ecec40516fa 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js @@ -246,6 +246,12 @@ define([ expect(type).toEqual('object'); }); + it('Must be false if "disabled" is true', function () { + obj.listVisible(false); + obj.disabled(true); + obj.toggleListVisible(); + expect(obj.listVisible()).toEqual(false); + }); it('Must be false if "listVisible" is true', function () { obj.listVisible(true); obj.toggleListVisible();