From d04e4d505aced753aedadf1f3593ef5420825857 Mon Sep 17 00:00:00 2001 From: Buba Suma Date: Wed, 19 Aug 2020 10:12:08 -0500 Subject: [PATCH 01/24] MC-36809: Secure flag is not set for frontend cookies on https - Fix secure cookie JS config is missing on frontend --- .../Test/StorefrontVerifySecureCookieTest.xml | 51 +++++++++++++++++++ .../StorefrontVerifyUnsecureCookieTest.xml | 40 +++++++++++++++ .../view/base/templates/html/cookie.phtml | 10 ++-- 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml create mode 100644 app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml new file mode 100644 index 0000000000000..56098cfec90cb --- /dev/null +++ b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifySecureCookieTest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + <description value="Verify that cookie are secure on storefront over https"/> + <severity value="MAJOR"/> + <testCaseId value="MC-36900"/> + <useCaseId value="MC-36809"/> + <group value="cookie"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/unsecure/base_url https://{$hostname}/" stepKey="setUnsecureBaseURL"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/unsecure/base_url http://{$hostname}/" stepKey="setUnsecureBaseURL"/> + <magentoCLI command="config:set web/secure/base_url http://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="useSecureURLsOnStorefront"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </after> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.cookiesConfig.secure ? 'true' : 'false'" stepKey="isCookieSecure"/> + <assertEquals stepKey="assertCookieIsSecure"> + <actualResult type="variable">isCookieSecure</actualResult> + <expectedResult type="string">true</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml new file mode 100644 index 0000000000000..e601a6b1920b0 --- /dev/null +++ b/app/code/Magento/Cookie/Test/Mftf/Test/StorefrontVerifyUnsecureCookieTest.xml @@ -0,0 +1,40 @@ +<?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="StorefrontVerifyUnsecureCookieTest"> + <annotations> + <features value="Cookie"/> + <stories value="Storefront Secure Cookie"/> + <title value="Verify Storefront Cookie Secure Config over http"/> + <description value="Verify that cookie are not secure on storefront over http"/> + <severity value="MAJOR"/> + <testCaseId value="MC-36899"/> + <useCaseId value="MC-36809"/> + <group value="cookie"/> + <group value="configuration"/> + </annotations> + <before> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </after> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.cookiesConfig.secure ? 'true' : 'false'" stepKey="isCookieSecure"/> + <assertEquals stepKey="assertCookieIsUnsecure"> + <actualResult type="variable">isCookieSecure</actualResult> + <expectedResult type="string">false</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml index f9d9c9071d69d..a604290004588 100644 --- a/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml +++ b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml @@ -10,9 +10,11 @@ * @var $block \Magento\Framework\View\Element\Js\Cookie * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ - -$scriptString = ' +$isCookieSecure = $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false'; +$scriptString = " window.cookiesConfig = window.cookiesConfig || {}; - window.cookiesConfig.secure = ' . /* @noEscape */ $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false'; + window.cookiesConfig.secure = $isCookieSecure; +"; +?> -echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> From 503c42874911024d704bbd68a24a215ac63ee369 Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Thu, 20 Aug 2020 10:40:36 -0500 Subject: [PATCH 02/24] MC-36755: Orders being archived prior to being processed - Checking at UI order has been checked excluding new order before submission --- .../ui_component/sales_order_grid.xml | 2 +- .../adminhtml/web/js/grid/tree-massactions.js | 34 +++++ .../js/grid/tree-massactions.test.js | 117 ++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Sales/view/adminhtml/web/js/grid/tree-massactions.js create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/grid/tree-massactions.test.js diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml index e1f047b372c95..f6b1240402477 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml @@ -58,7 +58,7 @@ </settings> </filterSelect> </filters> - <massaction name="listing_massaction" component="Magento_Ui/js/grid/tree-massactions"> + <massaction name="listing_massaction" component="Magento_Sales/js/grid/tree-massactions"> <action name="cancel"> <settings> <url path="sales/order/massCancel"/> diff --git a/app/code/Magento/Sales/view/adminhtml/web/js/grid/tree-massactions.js b/app/code/Magento/Sales/view/adminhtml/web/js/grid/tree-massactions.js new file mode 100644 index 0000000000000..a2783222afc28 --- /dev/null +++ b/app/code/Magento/Sales/view/adminhtml/web/js/grid/tree-massactions.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'mageUtils', + 'Magento_Ui/js/grid/tree-massactions' +], function (_, utils, Massactions) { + 'use strict'; + + return Massactions.extend({ + /** + * Overwrite Default action callback. + * Sends selections data with ids + * via POST request. + * + * @param {Object} action - Action data. + * @param {Object} data - Selections data. + */ + defaultCallback: function (action, data) { + var itemsType = 'selected', + selections = {}; + + selections[itemsType] = data[itemsType]; + _.extend(selections, data.params || {}); + utils.submit({ + url: action.url, + data: selections + }); + } + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/grid/tree-massactions.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/grid/tree-massactions.test.js new file mode 100644 index 0000000000000..7e33a7ad3c1fa --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/grid/tree-massactions.test.js @@ -0,0 +1,117 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/*eslint max-nested-callbacks: 0*/ +define([ + 'jquery', + 'squire', + 'underscore', + 'Magento_Sales/js/grid/tree-massactions' +], function ($, Squire, _, TreeMassaction) { + 'use strict'; + + var injector = new Squire(), + mocks = { + 'Magento_Ui/js/grid/massactions': { + defaultCallback: jasmine.createSpy().and.returnValue({}), + applyAction: jasmine.createSpy().and.returnValue({}) + } + }, + obj, + utils; + + describe('Magento_Sales/js/grid/tree-massactions', function () { + var model; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require([ + 'Magento_Ui/js/grid/massactions', + 'mageUtils' + ], function (instance, mageUtils) { + obj = _.extend({}, instance); + utils = mageUtils; + done(); + }); + model = new TreeMassaction({ + actions: [ + { + type: 'availability', + actions: [{ + type: 'enable' + }, { + type: 'disable' + }] + }, + { + type: 'hold_order', + component: 'uiComponent', + label: 'hold', + url: 'http://local.magento/hold_order', + modules: { + selections: ['1','2','3'] + }, + actions: [{ + callback: 'defaultCallback' + }] + }] + }); + }); + + afterEach(function () { + try { + injector.clean(); + injector.remove(); + } catch (e) {} + }); + + describe('check applyAction', function () { + it('change visibility of submenu', function () { + expect(model.actions()[0].visible()).toBeFalsy(); + expect(model.applyAction('availability')).toBe(model); + expect(model.actions()[0].visible()).toBeTruthy(); + }); + }); + describe('check defaultCallback', function () { + it('check model called with action and selected data', function () { + expect(model.applyAction('hold_order')).toBe(model); + expect(model.actions()[1].visible()).toBeTruthy(); + expect(model.actions()[1].modules.selections).toBeTruthy(); + expect(model.actions()[1].modules.selections.total).toBeFalsy(); + }); + + it('check defaultCallback submitted the data', function () { + var action = { + component: 'uiComponent', + label: 'Hold', + type: 'hold_order', + url: 'http://local.magento/hold_order/' + }, + data = { + excludeMode: true, + excluded: [], + params: {}, + selected: ['7', '6', '5', '4', '3', '2', '1'], + total: 7 + }, + result; + + obj.getAction = jasmine.createSpy().and.returnValue('hold_order'); + + obj.applyAction(action); + + result = obj.defaultCallback(action, data); + + expect(typeof result).toBe('object'); + spyOn(utils, 'submit').and.callThrough(); + utils.submit({ + url: action.url, + data: data.selected + }); + expect(utils.submit).toHaveBeenCalled(); + }); + }); + }); +}); From f1e461f80e16745c30970bc48d33864d1e939e80 Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Fri, 21 Aug 2020 12:35:08 -0500 Subject: [PATCH 03/24] MC-36721: Unable to export customers with custom gender attribute value - Adding attribute check for gender --- .../Component/DataProvider/DocumentTest.php | 41 ++++++++++++++++--- .../Ui/Component/DataProvider/Document.php | 23 ++++++++--- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php index e9bd30940a064..08fd76afb76d3 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php @@ -80,11 +80,16 @@ protected function setUp(): void } /** - * @covers \Magento\Customer\Ui\Component\DataProvider\Document::getCustomAttribute + * @dataProvider getGenderAttributeDataProvider + * @covers \Magento\Customer\Ui\Component\DataProvider\Document::getCustomAttribute + * @param int $genderId + * @param string $attributeValue + * @param string $attributeLabel */ - public function testGetGenderAttribute() + public function testGetGenderAttribute(int $genderId, string $attributeValue, string $attributeLabel): void { - $genderId = 1; + $expectedResult = !empty($attributeValue) ? $attributeLabel : $genderId; + $this->document->setData('gender', $genderId); $this->groupRepository->expects(static::never()) @@ -106,11 +111,37 @@ public function testGetGenderAttribute() ->willReturn([$genderId => $option]); $option->expects(static::once()) + ->method('getValue') + ->willReturn($attributeValue); + + $option->expects(static::any()) ->method('getLabel') - ->willReturn('Male'); + ->willReturn($attributeLabel); $attribute = $this->document->getCustomAttribute('gender'); - static::assertEquals('Male', $attribute->getValue()); + static::assertEquals($expectedResult, $attribute->getValue()); + } + + /** + * Data provider for testGetGenderAttribute + * @return array + */ + public function getGenderAttributeDataProvider() + { + return [ + 'with valid gender label and value' => [ + 1, '1', 'Male' + ], + 'with empty gender label' => [ + 2, '2', '' + ], + 'with empty gender value' => [ + 3, '', 'test' + ], + 'with empty gender label and value' => [ + 4, '', '' + ] + ]; } /** diff --git a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php index 468a9e7946f2d..e802505caf9d1 100644 --- a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php +++ b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php @@ -5,18 +5,23 @@ */ namespace Magento\Customer\Ui\Component\DataProvider; +use Exception; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\OptionInterface; +use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Customer\Model\AccountManagement; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; /** * Class Document + * + * Set the attribute label and value for UI Component + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\Document { @@ -127,7 +132,7 @@ public function getCustomAttribute($attributeCode) private function setGenderValue() { $value = $this->getData(self::$genderAttributeCode); - + if (!$value) { $this->setCustomAttribute(self::$genderAttributeCode, 'N/A'); return; @@ -135,8 +140,15 @@ private function setGenderValue() try { $attributeMetadata = $this->customerMetadata->getAttributeMetadata(self::$genderAttributeCode); - $option = $attributeMetadata->getOptions()[$value]; - $this->setCustomAttribute(self::$genderAttributeCode, $option->getLabel()); + $options = $attributeMetadata->getOptions(); + array_walk( + $options, + function (OptionInterface $option) use ($value) { + if ($option->getValue() == $value) { + $this->setCustomAttribute(self::$genderAttributeCode, $option->getLabel()); + } + } + ); } catch (NoSuchEntityException $e) { $this->setCustomAttribute(self::$genderAttributeCode, 'N/A'); } @@ -199,6 +211,7 @@ private function setConfirmationValue() * Update lock expires value. Method set account lock text value to match what is shown in grid * * @return void + * @throws Exception */ private function setAccountLockValue() { From dd929f86588b8ef39b60b3e0fe1bb592ca587ba7 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Tue, 25 Aug 2020 16:39:17 -0500 Subject: [PATCH 04/24] MC-36903: Records are not deleted when unassigning an item from a website which causes image duplication when executing POST rest/all/V1/products - Remove eav attributes stores values when product is unassigned from website --- .../Model/Product/Gallery/CreateHandler.php | 13 +- .../Model/Product/Gallery/UpdateHandler.php | 94 +++++++++- .../Model/ResourceModel/AttributeValue.php | 173 ++++++++++++++++++ .../Product/Gallery/UpdateHandlerTest.php | 102 ++++++++++- 4 files changed, 375 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index 225a3a4c44a9b..5fefcf995e0c7 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -596,10 +596,21 @@ private function canRemoveImage(ProductInterface $product, string $imageFile) :b $canRemoveImage = true; $gallery = $this->getImagesForAllStores($product); $storeId = $product->getStoreId(); + $storeIds = []; + $storeIds[] = 0; + $websiteIds = array_map('intval', $product->getWebsiteIds() ?? []); + foreach ($this->storeManager->getStores() as $store) { + if (in_array((int) $store->getWebsiteId(), $websiteIds, true)) { + $storeIds[] = (int) $store->getId(); + } + } if (!empty($gallery)) { foreach ($gallery as $image) { - if ($image['filepath'] === $imageFile && (int) $image['store_id'] !== $storeId) { + if (in_array((int) $image['store_id'], $storeIds) + && $image['filepath'] === $imageFile + && (int) $image['store_id'] !== $storeId + ) { $canRemoveImage = false; } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 049846ef36490..8061422d84288 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -5,17 +5,69 @@ */ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Media\Config; use Magento\Catalog\Model\ResourceModel\Product\Gallery; -use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\Eav\Model\ResourceModel\AttributeValue; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Filesystem; +use Magento\Framework\Json\Helper\Data; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Update handler for catalog product gallery. * * @api * @since 101.0.0 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler +class UpdateHandler extends CreateHandler { + /** + * @var AttributeValue + */ + private $attributeValue; + + /** + * @param MetadataPool $metadataPool + * @param ProductAttributeRepositoryInterface $attributeRepository + * @param Gallery $resourceModel + * @param Data $jsonHelper + * @param Config $mediaConfig + * @param Filesystem $filesystem + * @param Database $fileStorageDb + * @param StoreManagerInterface|null $storeManager + * @param AttributeValue|null $attributeValue + */ + public function __construct( + MetadataPool $metadataPool, + ProductAttributeRepositoryInterface $attributeRepository, + Gallery $resourceModel, + Data $jsonHelper, + Config $mediaConfig, + Filesystem $filesystem, + Database $fileStorageDb, + StoreManagerInterface $storeManager = null, + ?AttributeValue $attributeValue = null + ) { + parent::__construct( + $metadataPool, + $attributeRepository, + $resourceModel, + $jsonHelper, + $mediaConfig, + $filesystem, + $fileStorageDb, + $storeManager + ); + $this->attributeValue = $attributeValue ?: ObjectManager::getInstance()->get(AttributeValue::class); + } + /** * @inheritdoc * @@ -26,6 +78,7 @@ protected function processDeletedImages($product, array &$images) $filesToDelete = []; $recordsToDelete = []; $picturesInOtherStores = []; + $imagesToDelete = []; foreach ($this->resourceModel->getProductImages($product, $this->extractStoreIds($product)) as $image) { $picturesInOtherStores[$image['filepath']] = true; @@ -38,6 +91,7 @@ protected function processDeletedImages($product, array &$images) continue; } $recordsToDelete[] = $image['value_id']; + $imagesToDelete[] = $image['file']; $catalogPath = $this->mediaConfig->getBaseMediaPath(); $isFile = $this->mediaDirectory->isFile($catalogPath . $image['file']); // only delete physical files if they are not used by any other products and if this file exist @@ -48,8 +102,8 @@ protected function processDeletedImages($product, array &$images) } } + $this->deleteMediaAttributeValues($product, $imagesToDelete); $this->resourceModel->deleteGallery($recordsToDelete); - $this->removeDeletedImages($filesToDelete); } @@ -94,14 +148,14 @@ protected function processNewImage($product, array &$image) /** * Retrieve store ids from product. * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return array * @since 101.0.0 */ protected function extractStoreIds($product) { $storeIds = $product->getStoreIds(); - $storeIds[] = \Magento\Store\Model\Store::DEFAULT_STORE_ID; + $storeIds[] = Store::DEFAULT_STORE_ID; // Removing current storeId. $storeIds = array_flip($storeIds); @@ -125,5 +179,35 @@ protected function removeDeletedImages(array $files) foreach ($files as $filePath) { $this->mediaDirectory->delete($catalogPath . '/' . $filePath); } + return null; + } + + /** + * Delete media attributes values for given images + * + * @param Product $product + * @param string[] $images + */ + private function deleteMediaAttributeValues(Product $product, array $images): void + { + if ($images) { + $values = $this->attributeValue->getValues( + ProductInterface::class, + $product->getData($this->metadata->getLinkField()), + $this->mediaConfig->getMediaAttributeCodes() + ); + $valuesToDelete = []; + foreach ($values as $value) { + if (in_array($value['value'], $images, true)) { + $valuesToDelete[] = $value; + } + } + if ($valuesToDelete) { + $this->attributeValue->deleteValues( + ProductInterface::class, + $valuesToDelete + ); + } + } } } diff --git a/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php b/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php new file mode 100644 index 0000000000000..305ed202ff22b --- /dev/null +++ b/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model\ResourceModel; + +use Magento\Eav\Model\Config; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Entity attribute values resource + */ +class AttributeValue +{ + /** + * @var MetadataPool + */ + private $metadataPool; + /** + * @var ResourceConnection + */ + private $resourceConnection; + /** + * @var Config + */ + private $config; + + /** + * @param ResourceConnection $resourceConnection + * @param MetadataPool $metadataPool + * @param Config $config + */ + public function __construct( + ResourceConnection $resourceConnection, + MetadataPool $metadataPool, + Config $config + ) { + $this->resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + $this->config = $config; + } + + /** + * Get attribute values for given entity type, entity ID, attribute codes and store IDs + * + * @param string $entityType + * @param int $entityId + * @param string[] $attributeCodes + * @param int[] $storeIds + * @return array + */ + public function getValues( + string $entityType, + int $entityId, + array $attributeCodes = [], + array $storeIds = [] + ): array { + $metadata = $this->metadataPool->getMetadata($entityType); + $connection = $metadata->getEntityConnection(); + $selects = []; + $attributeTables = []; + $attributes = []; + $allAttributes = $this->getEntityAttributes($entityType); + $result = []; + if ($attributeCodes) { + foreach ($attributeCodes as $attributeCode) { + $attributes[$attributeCode] = $allAttributes[$attributeCode]; + } + } else { + $attributes = $allAttributes; + } + + foreach ($attributes as $attribute) { + if (!$attribute->isStatic()) { + $attributeTables[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId(); + } + } + + if ($attributeTables) { + foreach ($attributeTables as $attributeTable => $attributeIds) { + $select = $connection->select() + ->from( + ['t' => $attributeTable], + ['*'] + ) + ->where($metadata->getLinkField() . ' = ?', $entityId) + ->where('attribute_id IN (?)', $attributeIds); + if (!empty($storeIds)) { + $select->where( + 'store_id IN (?)', + $storeIds + ); + } + $selects[] = $select; + } + + if (count($selects) > 1) { + $select = $connection->select(); + $select->from(['u' => new UnionExpression($selects, Select::SQL_UNION_ALL, '( %s )')]); + } else { + $select = reset($selects); + } + + $result = $connection->fetchAll($select); + } + + return $result; + } + + /** + * Delete attribute values + * + * @param string $entityType + * @param array[][] $values + * Format: + * array( + * 0 => array( + * value_id => 1, + * attribute_id => 11 + * ), + * 1 => array( + * value_id => 2, + * attribute_id => 22 + * ) + * ) + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function deleteValues(string $entityType, array $values): void + { + $metadata = $this->metadataPool->getMetadata($entityType); + $connection = $metadata->getEntityConnection(); + $attributeTables = []; + $allAttributes = []; + + foreach ($this->getEntityAttributes($entityType) as $attribute) { + $allAttributes[(int) $attribute->getAttributeId()] = $attribute; + } + + foreach ($values as $value) { + $attribute = $allAttributes[(int) $value['attribute_id']] ?? null; + if ($attribute && !$attribute->isStatic()) { + $attributeTables[$attribute->getBackend()->getTable()][] = (int) $value['value_id']; + } + } + + foreach ($attributeTables as $attributeTable => $valueIds) { + $connection->delete( + $attributeTable, + [ + 'value_id IN (?)' => $valueIds + ] + ); + } + } + + /** + * Get attribute of given entity type + * + * @param string $entityType + */ + private function getEntityAttributes(string $entityType) + { + $metadata = $this->metadataPool->getMetadata($entityType); + $eavEntityType = $metadata->getEavEntityType(); + return null === $eavEntityType ? [] : $this->config->getEntityAttributes($eavEntityType); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index f9d235493297f..2659f14c07c7a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -20,6 +20,7 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; /** @@ -79,6 +80,15 @@ class UpdateHandlerTest extends \PHPUnit\Framework\TestCase */ private $mediaAttributeId; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** + * @var int + */ + private $currentStoreId; + /** * @inheritdoc */ @@ -93,6 +103,8 @@ protected function setUp(): void $this->productResource = $this->objectManager->create(ProductResource::class); $this->mediaAttributeId = (int)$this->productResource->getAttribute('media_gallery')->getAttributeId(); $this->config = $this->objectManager->get(Config::class); + $this->storeManager = $this->objectManager->create(StoreManagerInterface::class); + $this->currentStoreId = $this->storeManager->getStore()->getId(); $this->mediaDirectory = $this->objectManager->get(Filesystem::class) ->getDirectoryWrite(DirectoryList::MEDIA); $this->mediaDirectory->writeFile($this->fileName, 'Test'); @@ -274,7 +286,7 @@ public function testExecuteWithImageToDelete(): void $this->updateHandler->execute($product); $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $this->mediaAttributeId); $this->assertCount(0, $productImages); - $this->assertFileNotExists( + $this->assertFileDoesNotExist( $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $image) ); $defaultImages = $this->productResource->getAttributeRawValue( @@ -344,6 +356,7 @@ public function testExecuteWithTwoImagesOnStoreView(): void */ protected function tearDown(): void { + $this->storeManager->setCurrentStore($this->currentStoreId); parent::tearDown(); $this->mediaDirectory->getDriver()->deleteFile($this->mediaDirectory->getAbsolutePath($this->fileName)); $this->galleryResource->getConnection() @@ -377,4 +390,91 @@ private function updateProductGalleryImages(ProductInterface $product, array $im $product->setData('store_id', Store::DEFAULT_STORE_ID); $product->setData('media_gallery', ['images' => ['image' => array_merge($image, $imageData)]]); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDbIsolation disabled + * @return void + */ + public function testDeleteWithMultiWebsites(): void + { + $defaultWebsiteId = (int) $this->storeManager->getWebsite('base')->getId(); + $secondWebsiteId = (int) $this->storeManager->getWebsite('test')->getId(); + $defaultStoreId = (int) $this->storeManager->getStore('default')->getId(); + $secondStoreId = (int) $this->storeManager->getStore('fixture_second_store')->getId(); + $imageRoles = ['image', 'small_image', 'thumbnail']; + $globalScopeId = Store::DEFAULT_STORE_ID; + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product = $this->getProduct($globalScopeId); + // Assert that product has images + $this->assertNotEmpty($product->getMediaGalleryEntries()); + $image = $product->getImage(); + $path = $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $image); + $this->assertFileExists($path); + // Assign product to default and second website and save changes + $product->setWebsiteIds([$defaultWebsiteId, $secondWebsiteId]); + $this->productRepository->save($product); + // Assert that product image has roles in global scope only + $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['image']); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['small_image']); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['thumbnail']); + $this->assertArrayNotHasKey($defaultStoreId, $imageRolesPerStore); + $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); + // Assign roles to product image on second store and save changes + $this->storeManager->setCurrentStore($secondStoreId); + $product = $this->getProduct($secondStoreId); + $product->addData(array_fill_keys($imageRoles, $image)); + $this->productRepository->save($product); + // Assert that roles are assigned to product image for second store + $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['image']); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['small_image']); + $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['thumbnail']); + $this->assertArrayNotHasKey($defaultStoreId, $imageRolesPerStore); + $this->assertEquals($image, $imageRolesPerStore[$secondStoreId]['image']); + $this->assertEquals($image, $imageRolesPerStore[$secondStoreId]['small_image']); + $this->assertEquals($image, $imageRolesPerStore[$secondStoreId]['thumbnail']); + // Delete existing images and save changes + $this->storeManager->setCurrentStore($globalScopeId); + $product = $this->getProduct($globalScopeId); + $product->setMediaGalleryEntries([]); + $this->productRepository->save($product); + $product = $this->getProduct($globalScopeId); + // Assert that image was not deleted as it has roles in second store + $this->assertNotEmpty($product->getMediaGalleryEntries()); + $this->assertFileExists($path); + // Unlink second website, delete existing images and save changes + $product->setWebsiteIds([$defaultWebsiteId]); + $product->setMediaGalleryEntries([]); + $this->productRepository->save($product); + $product = $this->getProduct($globalScopeId); + // Assert that image was deleted and product has no images + $this->assertEmpty($product->getMediaGalleryEntries()); + $this->assertFileDoesNotExist($path); + // Load image roles + $imageRolesPerStore = $this->getProductStoreImageRoles($product); + // Assert that image roles are reset on global scope and removed on second store + // as the product is no longer assigned to second website + $this->assertEquals('no_selection', $imageRolesPerStore[$globalScopeId]['image']); + $this->assertEquals('no_selection', $imageRolesPerStore[$globalScopeId]['small_image']); + $this->assertEquals('no_selection', $imageRolesPerStore[$globalScopeId]['thumbnail']); + $this->assertArrayNotHasKey($defaultStoreId, $imageRolesPerStore); + $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); + } + + /** + * @param Product $product + * @return array + */ + private function getProductStoreImageRoles(Product $product): array + { + $imageRolesPerStore = []; + $stores = array_keys($this->storeManager->getStores(true)); + foreach ($this->galleryResource->getProductImages($product, $stores) as $role) { + $imageRolesPerStore[$role['store_id']][$role['attribute_code']] = $role['filepath']; + } + return $imageRolesPerStore; + } } From f9006ff4c89831b6c854387fe7838d3c95827178 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Fri, 28 Aug 2020 15:48:01 -0500 Subject: [PATCH 05/24] MC-36978: Invalid Character Customer Account Create DOB --- lib/web/mage/utils/misc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web/mage/utils/misc.js b/lib/web/mage/utils/misc.js index b1c0c33324c28..148206d9ad69d 100644 --- a/lib/web/mage/utils/misc.js +++ b/lib/web/mage/utils/misc.js @@ -282,7 +282,7 @@ define([ var newFormat; newFormat = format.replace(/yyyy|yy|y/, 'YYYY'); // replace the year - newFormat = newFormat.replace(/dd|d/g, 'DD'); // replace the date + newFormat = newFormat.replace(/dd|d/g, 'D'); // replace the date return newFormat; }, From 4ea7db83579a89cfd7053c3d9763c8ec57477350 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Mon, 31 Aug 2020 16:10:08 -0500 Subject: [PATCH 06/24] MC-36978: Invalid Character Customer Account Create DOB --- .../view/frontend/web/js/validation.js | 2 +- .../Customer/frontend/js/validation.test.js | 158 ++++++++++++++++++ lib/web/mage/utils/misc.js | 2 +- 3 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js diff --git a/app/code/Magento/Customer/view/frontend/web/js/validation.js b/app/code/Magento/Customer/view/frontend/web/js/validation.js index 9ffc8137135da..6b9983c0af873 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/validation.js +++ b/app/code/Magento/Customer/view/frontend/web/js/validation.js @@ -11,7 +11,7 @@ define([ $.validator.addMethod( 'validate-date', function (value, element, params) { - var dateFormat = utils.convertToMomentFormat(params.dateFormat); + var dateFormat = utils.normalizeDate(params.dateFormat); if (value === '') { return true; diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js new file mode 100644 index 0000000000000..57b3557f40c15 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js @@ -0,0 +1,158 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Customer/js/validation' +], function ($) { + 'use strict'; + + describe('Testing Customer/view/frontend/web/js/validation.js', function () { + var params, + dataProvider; + + dataProvider = [ + { + format: 'dd.MM.yy.', + date: '09.02.18.', + expects: true + }, + { + format: 'd/MM/y', + date: '9/02/2018', + expects: true + }, + { + format: 'MM/dd/yy', + date: '02/09/18', + expects: true + }, + { + format: 'M/d/yy', + date: '2/9/18', + expects: true + }, + { + format: 'yy-MM-dd', + date: '18-02-09', + expects: true + }, + { + format: 'dd.MM.y.', + date: '09.02.2018.', + expects: true + }, + { + format: 'y. MM. dd.', + date: '2018. 02. 09.', + expects: true + }, + { + format: 'd/MM/yy', + date: '9/02/18', + expects: true + }, + { + format: 'dd-MM-yy', + date: '09-02-18', + expects: true + }, + { + format: 'dd/MM/yy', + date: '09/02/18', + expects: true + }, + { + format: 'dd.MM.y', + date: '09.02.2018', + expects: true + }, + { + format: 'd. MM. yy', + date: '9. 02. 18', + expects: true + }, + { + format: 'dd/MM/y', + date: '09/02/2018', + expects: true + }, + { + format: 'd.MM.y', + date: '9.02.2018', + expects: true + }, + { + format: 'd.M.yy', + date: '9.2.18', + expects: true + }, + { + format: 'd.MM.yy г.', + date: '9.02.18 г.', + expects: true + }, + { + format: 'dd.M.yy', + date: '09.2.18', + expects: true + }, + { + format: 'y-MM-dd', + date: '2018-02-09', + expects: true + }, + { + format: 'd.M.yy.', + date: '9.2.18.', + expects: true + }, + { + format: 'd.M.y', + date: '9.2.2018', + expects: true + }, + { + format: 'd/M/y', + date: '9/2/2018', + expects: true + }, + { + format: 'yy/M/d', + date: '19/2/9', + expects: true + }, + { + format: 'd/M/yy', + date: '9/2/18', + expects: true + }, + { + format: 'y/M/d', + date: '2018/2/9', + expects: true + }, + { + format: 'y/MM/dd', + date: '2018/02/09', + expects: true + }, + { + format: 'yy. M. d.', + date: '18. 2. 9.', + expects: true + } + ]; + + dataProvider.forEach(function (data) { + it('Test date validation for format ' + data.format, function () { + params = { + 'dateFormat': data.format + }; + expect($.validator.methods['validate-date'] + .call($.validator.prototype, data.date, null, params)).toEqual(data.expects); + }); + }); + }); +}); diff --git a/lib/web/mage/utils/misc.js b/lib/web/mage/utils/misc.js index 148206d9ad69d..b1c0c33324c28 100644 --- a/lib/web/mage/utils/misc.js +++ b/lib/web/mage/utils/misc.js @@ -282,7 +282,7 @@ define([ var newFormat; newFormat = format.replace(/yyyy|yy|y/, 'YYYY'); // replace the year - newFormat = newFormat.replace(/dd|d/g, 'D'); // replace the date + newFormat = newFormat.replace(/dd|d/g, 'DD'); // replace the date return newFormat; }, From a95ee525c1c0c9aa7aee8a813ed94a3e52b7b256 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Mon, 31 Aug 2020 16:53:25 -0500 Subject: [PATCH 07/24] MC-36978: Invalid Character Customer Account Create DOB --- .../app/code/Magento/Customer/frontend/js/validation.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js index 57b3557f40c15..1b82e0ba36e16 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js @@ -2,6 +2,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* eslint-disable max-nested-callbacks */ define([ 'jquery', 'Magento_Customer/js/validation' From 269089f9b6be72fa146c86452b618efc37e4fcfc Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Mon, 31 Aug 2020 17:43:47 -0500 Subject: [PATCH 08/24] MC-36978: Invalid Character Customer Account Create DOB --- .../Customer/frontend/js/validation.test.js | 130 ++---------------- 1 file changed, 10 insertions(+), 120 deletions(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js index 1b82e0ba36e16..847bb4a040bd7 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js @@ -10,140 +10,30 @@ define([ ], function ($) { 'use strict'; - describe('Testing Customer/view/frontend/web/js/validation.js', function () { + describe('Testing customer DOB validation to tolerate zeroes in the single digit dates', function () { var params, dataProvider; dataProvider = [ { - format: 'dd.MM.yy.', - date: '09.02.18.', - expects: true - }, - { - format: 'd/MM/y', - date: '9/02/2018', - expects: true - }, - { - format: 'MM/dd/yy', - date: '02/09/18', - expects: true - }, - { - format: 'M/d/yy', - date: '2/9/18', - expects: true - }, - { - format: 'yy-MM-dd', - date: '18-02-09', - expects: true - }, - { - format: 'dd.MM.y.', - date: '09.02.2018.', - expects: true - }, - { - format: 'y. MM. dd.', - date: '2018. 02. 09.', - expects: true - }, - { - format: 'd/MM/yy', - date: '9/02/18', - expects: true - }, - { - format: 'dd-MM-yy', - date: '09-02-18', - expects: true - }, - { - format: 'dd/MM/yy', - date: '09/02/18', - expects: true - }, - { - format: 'dd.MM.y', - date: '09.02.2018', - expects: true - }, - { - format: 'd. MM. yy', - date: '9. 02. 18', - expects: true - }, - { - format: 'dd/MM/y', - date: '09/02/2018', - expects: true - }, - { - format: 'd.MM.y', - date: '9.02.2018', - expects: true - }, - { - format: 'd.M.yy', - date: '9.2.18', - expects: true - }, - { - format: 'd.MM.yy г.', - date: '9.02.18 г.', - expects: true - }, - { - format: 'dd.M.yy', - date: '09.2.18', - expects: true - }, - { - format: 'y-MM-dd', - date: '2018-02-09', - expects: true - }, - { - format: 'd.M.yy.', - date: '9.2.18.', - expects: true - }, - { - format: 'd.M.y', - date: '9.2.2018', - expects: true - }, - { - format: 'd/M/y', - date: '9/2/2018', - expects: true - }, - { - format: 'yy/M/d', - date: '19/2/9', - expects: true - }, - { - format: 'd/M/yy', + format: 'M/d/Y', date: '9/2/18', expects: true }, { - format: 'y/M/d', - date: '2018/2/9', - expects: true + format: 'M/DD/Y', + date: '9/2/18', + expects: false }, { - format: 'y/MM/dd', - date: '2018/02/09', + format: 'MM/DD/Y', + date: '09/02/18', expects: true }, { - format: 'yy. M. d.', - date: '18. 2. 9.', - expects: true + format: 'MM/DD/YYYY', + date: '09/2/18', + expects: false } ]; From 194c62ed34ce19615c6ccc5c4822257523761805 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Mon, 31 Aug 2020 17:57:07 -0500 Subject: [PATCH 09/24] MC-36978: Invalid Character Customer Account Create DOB --- .../Customer/frontend/js/validation.test.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js index 847bb4a040bd7..df1fb609f8bda 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js @@ -17,24 +17,14 @@ define([ dataProvider = [ { format: 'M/d/Y', - date: '9/2/18', + date: '09/2/18', expects: true }, { format: 'M/DD/Y', - date: '9/2/18', - expects: false - }, - { - format: 'MM/DD/Y', - date: '09/02/18', - expects: true - }, - { - format: 'MM/DD/YYYY', date: '09/2/18', expects: false - } + }, ]; dataProvider.forEach(function (data) { From 51b77f09bb2d5d1279bf7f12f63cdb580fd9689c Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko <lytvynen@adobe.com> Date: Tue, 1 Sep 2020 06:32:01 -0500 Subject: [PATCH 10/24] MC-36978: Invalid Character Customer Account Create DOB --- .../app/code/Magento/Customer/frontend/js/validation.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js index df1fb609f8bda..c830632ed0e87 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/validation.test.js @@ -24,7 +24,7 @@ define([ format: 'M/DD/Y', date: '09/2/18', expects: false - }, + } ]; dataProvider.forEach(function (data) { From e4c0a818f6f02c057dcb3de7d3cf9cd353825dca Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Wed, 2 Sep 2020 18:13:38 -0500 Subject: [PATCH 11/24] MC-36214: Issue with saving video position in the first time - Fix new product image/video position is changed after saving product --- .../view/adminhtml/web/js/product-gallery.js | 11 +- .../adminhtml/js/product-gallery.test.js | 132 ++++++++++++++++++ 2 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/Catalog/adminhtml/js/product-gallery.test.js diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/product-gallery.js b/app/code/Magento/Catalog/view/adminhtml/web/js/product-gallery.js index b2a12bea30150..1af0f10770c41 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/product-gallery.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/product-gallery.js @@ -188,12 +188,17 @@ define([ _addItem: function (event, imageData) { var count = this.element.find(this.options.imageSelector).length, element, - imgElement; + imgElement, + position = count + 1, + lastElement = this.element.find(this.options.imageSelector + ':last'); + if (lastElement.length === 1) { + position = parseInt(lastElement.data('imageData').position || count, 10) + 1; + } imageData = $.extend({ 'file_id': imageData['value_id'] ? imageData['value_id'] : Math.random().toString(33).substr(2, 18), 'disabled': imageData.disabled ? imageData.disabled : 0, - 'position': count + 1, + 'position': position, sizeLabel: bytesToSize(imageData.size) }, imageData); @@ -206,7 +211,7 @@ define([ if (count === 0) { element.prependTo(this.element); } else { - element.insertAfter(this.element.find(this.options.imageSelector + ':last')); + element.insertAfter(lastElement); } if (!this.options.initialized && diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/adminhtml/js/product-gallery.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/adminhtml/js/product-gallery.test.js new file mode 100644 index 0000000000000..2d6b6cc88fe78 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/adminhtml/js/product-gallery.test.js @@ -0,0 +1,132 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*eslint-disable max-nested-callbacks*/ +/*jscs:disable jsDoc*/ +define([ + 'jquery', + 'Magento_Catalog/js/product-gallery' +], function ($) { + 'use strict'; + + var galleryEl, + defaultConfig = { + images: [ + { + disabled: 0, + file: '/e/a/earth.jpg', + position: 2, + url: 'http://localhost/media/catalog/product/e/a/earth.jpg', + size: 2048, + 'value_id': 2 + }, + { + disabled: 0, + file: '/m/a/mars.jpg', + position: 3, + url: 'http://localhost/media/catalog/product/m/a/mars.jpg', + size: 3072, + 'value_id': 3 + }, + { + disabled: 0, + file: '/j/u/jupiter.jpg', + position: 5, + size: 5120, + url: 'http://localhost/media/catalog/product/j/u/jupiter.jpg', + 'value_id': 5 + } + ], + types: { + 'image': { + code: 'image', + label: 'Base', + name: 'product[image]' + }, + 'small_image': { + code: 'small_image', + label: 'Small', + name: 'product[image]' + }, + 'thumbnail': { + code: 'thumbnail', + label: 'Thumbnail', + name: 'product[image]' + } + } + }; + + function init(config) { + $(galleryEl).productGallery($.extend({}, defaultConfig, config || {})); + } + + beforeEach(function () { + $('<form>' + + '<div id="media_gallery_content" class="gallery">' + + '<script id="media_gallery_content-template" data-template="image" type="text/x-magento-template">' + + '<div class="image item <% if(data.disabled == 1){ %>hidden-for-front<% } %>" data-role="image">' + + '<input type="hidden" name="product[media_gallery][images][<%- data.file_id %>][position]"' + + ' value="<%- data.position %>" data-form-part="product_form" class="position"/>' + + '<input type="hidden" name="product[media_gallery][images][<%- data.file_id %>][file]"' + + ' value="<%- data.file %>" data-form-part="product_form"/>' + + '<input type="hidden" name="product[media_gallery][images][<%- data.file_id %>][label]"' + + ' value="<%- data.label %>" data-form-part="product_form"/>' + + '<div class="product-image-wrapper">' + + '<img class="product-image" data-role="image-element" src="<%- data.url %>" alt=""/>' + + '<div class="actions"></div>' + + '</div>' + + '</div>' + + '</script>' + + '</div>' + + '</form>' + ).appendTo(document.body); + galleryEl = document.getElementById('media_gallery_content'); + }); + + afterEach(function () { + $(galleryEl).remove(); + galleryEl = undefined; + }); + + describe('Magento_Catalog/js/product-gallery', function () { + describe('_create()', function () { + it('check that existing images are rendered correctly', function () { + init(); + expect($(galleryEl).find('[data-role=image]').length).toBe(3); + expect($(galleryEl).find('[data-role=image]:nth-child(1) .position').val()).toBe('2'); + expect($(galleryEl).find('[data-role=image]:nth-child(2) .position').val()).toBe('3'); + expect($(galleryEl).find('[data-role=image]:nth-child(3) .position').val()).toBe('5'); + }); + }); + describe('_addItem()', function () { + it('check that new image is inserted at the first position if there were no existing images', function () { + init({ + images: [] + }); + $(galleryEl).trigger('addItem', { + file: '/s/a/saturn.jpg.tmp', + name: 'saturn.jpg', + size: 1024, + type: 'image/jpeg', + url: 'http://localhost/media/tmp/catalog/product/s/a/saturn.jpg' + }); + expect($(galleryEl).find('[data-role=image]').length).toBe(1); + expect($(galleryEl).find('[data-role=image]:nth-child(1) .position').val()).toBe('1'); + }); + it('check that new image is inserted at the last position if there were existing images', function () { + init(); + $(galleryEl).trigger('addItem', { + file: '/s/a/saturn.jpg.tmp', + name: 'saturn.jpg', + size: 1024, + type: 'image/jpeg', + url: 'http://localhost/media/tmp/catalog/product/s/a/saturn.jpg' + }); + expect($(galleryEl).find('[data-role=image]').length).toBe(4); + // check that new image position is the position of previous image in the list plus one + expect($(galleryEl).find('[data-role=image]:nth-child(4) .position').val()).toBe('6'); + }); + }); + }); +}); From b308c33d0e0eeebbe5e3019263d4fcc724f36c86 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Fri, 4 Sep 2020 16:27:14 -0500 Subject: [PATCH 12/24] MC-36942: HTML minification strips triple slashes from html string in phtml --- lib/internal/Magento/Framework/View/Template/Html/Minifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/View/Template/Html/Minifier.php b/lib/internal/Magento/Framework/View/Template/Html/Minifier.php index cdbce9d102a89..dba81a1547c44 100644 --- a/lib/internal/Magento/Framework/View/Template/Html/Minifier.php +++ b/lib/internal/Magento/Framework/View/Template/Html/Minifier.php @@ -144,7 +144,7 @@ function ($match) use (&$heredocs) { '#(?<!:|\\\\|\'|"|/)//(?!/)(?!\s*\<\!\[)(?!\s*]]\>)[^\n\r]*#', '', preg_replace( - '#(?<!:|\'|")//[^\n\r]*(\?\>)#', + '#(?<!:|\'|")//[^\n\r\<\?]*(\?\>)#', ' $1', preg_replace( '#(?<!:)//[^\n\r]*(\<\?php)[^\n\r]*(\s\?\>)[^\n\r]*#', From dbd414ca9fc800b9dbedf166c18993930e345b7e Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Fri, 28 Aug 2020 18:30:50 -0500 Subject: [PATCH 13/24] MC-33626: Unable to add product with multi-source inventory to wishlist with - Add multi-source inventory for wishlist module --- .../Model/ResourceModel/StockStatusFilter.php | 74 +++++++++++++++++++ .../StockStatusFilterInterface.php | 34 +++++++++ app/code/Magento/CatalogInventory/etc/di.xml | 1 + .../Model/ResourceModel/Item/Collection.php | 54 ++++++++++---- app/code/Magento/Wishlist/Model/Wishlist.php | 44 +++-------- .../Wishlist/Test/Unit/Model/WishlistTest.php | 60 +++++++++++---- .../_files/wishlist_with_disabled_product.php | 6 +- .../_files/wishlist_with_simple_product.php | 5 ++ 8 files changed, 212 insertions(+), 66 deletions(-) create mode 100644 app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php create mode 100644 app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilterInterface.php diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php new file mode 100644 index 0000000000000..e9497a1d44861 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Model\ResourceModel; + +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\Stock; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; + +/** + * Generic in-stock status filter + */ +class StockStatusFilter implements StockStatusFilterInterface +{ + private const TABLE_NAME = 'cataloginventory_stock_status'; + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @param ResourceConnection $resource + * @param StockConfigurationInterface $stockConfiguration + */ + public function __construct( + ResourceConnection $resource, + StockConfigurationInterface $stockConfiguration + ) { + $this->resource = $resource; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * @inheritDoc + */ + public function execute( + Select $select, + string $productTableAlias, + string $stockStatusTableAlias = self::TABLE_ALIAS, + ?int $websiteId = null + ): Select { + $stockStatusTable = $this->resource->getTableName(self::TABLE_NAME); + $joinCondition = [ + "{$stockStatusTableAlias}.product_id = {$productTableAlias}.entity_id", + $select->getConnection()->quoteInto( + "{$stockStatusTableAlias}.website_id = ?", + $this->stockConfiguration->getDefaultScopeId() + ), + $select->getConnection()->quoteInto( + "{$stockStatusTableAlias}.stock_id = ?", + Stock::DEFAULT_STOCK_ID + ) + ]; + $select->join( + [$stockStatusTableAlias => $stockStatusTable], + implode(' AND ', $joinCondition), + [] + ); + $select->where("{$stockStatusTableAlias}.stock_status = ?", StockStatusInterface::STATUS_IN_STOCK); + + return $select; + } +} diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilterInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilterInterface.php new file mode 100644 index 0000000000000..26eb4b0fa38eb --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilterInterface.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Model\ResourceModel; + +use Magento\Framework\DB\Select; + +/** + * In stock status filter interface. + */ +interface StockStatusFilterInterface +{ + public const TABLE_ALIAS = 'stock_status'; + + /** + * Add in-stock status constraint to the select. + * + * @param Select $select + * @param string $productTableAliasAlias + * @param string $stockStatusTableAlias + * @param int|null $websiteId + * @return Select + */ + public function execute( + Select $select, + string $productTableAliasAlias, + string $stockStatusTableAlias = self::TABLE_ALIAS, + ?int $websiteId = null + ): Select; +} diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index 751fa465bdb17..d2807249cf574 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -32,6 +32,7 @@ <preference for="Magento\CatalogInventory\Model\Spi\StockStateProviderInterface" type="Magento\CatalogInventory\Model\StockStateProvider" /> <preference for="Magento\CatalogInventory\Model\ResourceModel\QtyCounterInterface" type="Magento\CatalogInventory\Model\ResourceModel\Stock" /> + <preference for="Magento\CatalogInventory\Model\ResourceModel\StockStatusFilterInterface" type="Magento\CatalogInventory\Model\ResourceModel\StockStatusFilter" /> <type name="Magento\Catalog\Model\Product\Attribute\Repository"> <plugin name="filterCustomAttribute" type="Magento\CatalogInventory\Model\Plugin\FilterCustomAttribute" /> </type> diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 5d9b1911bc292..0fda2ef31347d 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -7,7 +7,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; -use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogInventory\Model\ResourceModel\StockStatusFilterInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Sales\Model\ConfigInterface; @@ -162,6 +162,17 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @var CollectionBuilderInterface */ private $productCollectionBuilder; + /** + * @var StockStatusFilterInterface + */ + private $stockStatusFilter; + + /** + * Whether product table is joined in select + * + * @var bool + */ + private $isProductTableJoined = false; /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory @@ -181,10 +192,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @param \Magento\Catalog\Model\Entity\AttributeFactory $catalogAttrFactory * @param \Magento\Wishlist\Model\ResourceModel\Item $resource * @param \Magento\Framework\App\State $appState - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * @param TableMaintainer|null $tableMaintainer * @param ConfigInterface|null $salesConfig * @param CollectionBuilderInterface|null $productCollectionBuilder + * @param StockStatusFilterInterface|null $stockStatusFilter * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -208,7 +220,8 @@ public function __construct( \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, TableMaintainer $tableMaintainer = null, ConfigInterface $salesConfig = null, - ?CollectionBuilderInterface $productCollectionBuilder = null + ?CollectionBuilderInterface $productCollectionBuilder = null, + ?StockStatusFilterInterface $stockStatusFilter = null ) { $this->stockConfiguration = $stockConfiguration; $this->_adminhtmlSales = $adminhtmlSales; @@ -227,6 +240,8 @@ public function __construct( $this->salesConfig = $salesConfig ?: ObjectManager::getInstance()->get(ConfigInterface::class); $this->productCollectionBuilder = $productCollectionBuilder ?: ObjectManager::getInstance()->get(CollectionBuilderInterface::class); + $this->stockStatusFilter = $stockStatusFilter + ?: ObjectManager::getInstance()->get(StockStatusFilterInterface::class); } /** @@ -368,15 +383,8 @@ protected function _renderFiltersBefore() $connection = $this->getConnection(); if ($this->_productInStock && !$this->stockConfiguration->isShowOutOfStock()) { - $inStockConditions = [ - "stockItem.product_id = {$mainTableName}.product_id", - $connection->quoteInto('stockItem.stock_status = ?', Stock::STOCK_IN_STOCK), - ]; - $this->getSelect()->join( - ['stockItem' => $this->getTable('cataloginventory_stock_status')], - join(' AND ', $inStockConditions), - [] - ); + $this->joinProductTable(); + $this->stockStatusFilter->execute($this->getSelect(), 'product_entity', 'stockItem'); } if ($this->_productVisible) { @@ -583,12 +591,9 @@ protected function _joinProductNameTable() $entityMetadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); $linkField = $entityMetadata->getLinkField(); + $this->joinProductTable(); $this->getSelect()->join( - ['product_entity' => $this->getTable('catalog_product_entity')], - 'product_entity.entity_id = main_table.product_id', - [] - )->join( ['product_name_table' => $attribute->getBackendTable()], 'product_name_table.' . $linkField . ' = product_entity.' . $linkField . ' AND product_name_table.store_id = ' . @@ -673,4 +678,21 @@ protected function _afterLoadData() return $this; } + + /** + * Join product table to select if not already joined + * + * @return void + */ + private function joinProductTable(): void + { + if (!$this->isProductTableJoined) { + $this->getSelect()->join( + ['product_entity' => $this->getTable('catalog_product_entity')], + 'product_entity.entity_id = main_table.product_id', + [] + ); + $this->isProductTableJoined = true; + } + } } diff --git a/app/code/Magento/Wishlist/Model/Wishlist.php b/app/code/Magento/Wishlist/Model/Wishlist.php index 437b3c757f9cf..f544dd374d734 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist.php @@ -12,9 +12,8 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; -use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Api\StockRegistryInterface; -use Magento\CatalogInventory\Model\Configuration; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; @@ -27,7 +26,6 @@ use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\DateTime; -use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Wishlist\Helper\Data; @@ -150,14 +148,9 @@ class Wishlist extends AbstractModel implements IdentityInterface private $serializer; /** - * @var ScopeConfigInterface + * @var StockConfigurationInterface */ - private $scopeConfig; - - /** - * @var StockRegistryInterface|null - */ - private $stockRegistry; + private $stockConfiguration; /** * Constructor @@ -181,8 +174,9 @@ class Wishlist extends AbstractModel implements IdentityInterface * @param Json|null $serializer * @param StockRegistryInterface|null $stockRegistry * @param ScopeConfigInterface|null $scopeConfig - * + * @param StockConfigurationInterface|null $stockConfiguration * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, @@ -203,7 +197,8 @@ public function __construct( array $data = [], Json $serializer = null, StockRegistryInterface $stockRegistry = null, - ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + ?StockConfigurationInterface $stockConfiguration = null ) { $this->_useCurrentWebsite = $useCurrentWebsite; $this->_catalogProduct = $catalogProduct; @@ -218,8 +213,8 @@ public function __construct( $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->productRepository = $productRepository; - $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); - $this->stockRegistry = $stockRegistry ?: ObjectManager::getInstance()->get(StockRegistryInterface::class); + $this->stockConfiguration = $stockConfiguration + ?: ObjectManager::getInstance()->get(StockConfigurationInterface::class); } /** @@ -467,7 +462,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false throw new LocalizedException(__('Cannot specify product.')); } - if ($this->isInStock($productId)) { + if (!$this->stockConfiguration->isShowOutOfStock($storeId) && !$product->getIsSalable()) { throw new LocalizedException(__('Cannot add product without stock to wishlist.')); } @@ -672,25 +667,6 @@ public function isSalable() return false; } - /** - * Retrieve if product has stock or config is set for showing out of stock products - * - * @param int $productId - * - * @return bool - */ - private function isInStock($productId) - { - /** @var StockItemInterface $stockItem */ - $stockItem = $this->stockRegistry->getStockItem($productId); - $showOutOfStock = $this->scopeConfig->isSetFlag( - Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - ScopeInterface::SCOPE_STORE - ); - $isInStock = $stockItem ? $stockItem->getIsInStock() : false; - return !$isInStock && !$showOutOfStock; - } - /** * Check customer is owner this wishlist * diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php index e09491813877b..369f77e527287 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type\AbstractType; use Magento\Catalog\Model\ProductFactory; +use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\Stock\Item as StockItem; use Magento\CatalogInventory\Model\Stock\StockItemRepository; @@ -132,6 +133,10 @@ class WishlistTest extends TestCase * @var StockRegistryInterface|MockObject */ private $stockRegistry; + /** + * @var StockConfigurationInterface|MockObject + */ + private $stockConfiguration; protected function setUp(): void { @@ -194,6 +199,8 @@ protected function setUp(): void ->method('getEventDispatcher') ->willReturn($this->eventDispatcher); + $this->stockConfiguration = $this->createMock(StockConfigurationInterface::class); + $this->wishlist = new Wishlist( $context, $this->registry, @@ -213,7 +220,8 @@ protected function setUp(): void [], $this->serializer, $this->stockRegistry, - $this->scopeConfig + $this->scopeConfig, + $this->stockConfiguration ); } @@ -300,6 +308,7 @@ public function testUpdateItem($itemId, $buyRequest, $param): void $newProduct->expects($this->once()) ->method('getTypeInstance') ->willReturn($instanceType); + $newProduct->expects($this->any())->method('getIsSalable')->willReturn(true); $item = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() @@ -388,8 +397,19 @@ public function updateItemDataProvider(): array ]; } - public function testAddNewItem() + /** + * @param bool $getIsSalable + * @param bool $isShowOutOfStock + * @param string $throwException + * + * @dataProvider addNewItemDataProvider + */ + public function testAddNewItem(bool $getIsSalable, bool $isShowOutOfStock, string $throwException): void { + if ($throwException) { + $this->expectExceptionMessage($throwException); + } + $this->stockConfiguration->method('isShowOutOfStock')->willReturn($isShowOutOfStock); $productId = 1; $storeId = 1; $buyRequest = json_encode( @@ -407,34 +427,31 @@ public function testAddNewItem() $instanceType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMock(); - $instanceType->expects($this->once()) - ->method('processConfiguration') + $instanceType->method('processConfiguration') ->willReturn('product'); $productMock = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getId', 'hasWishlistStoreId', 'getStoreId', 'getTypeInstance']) + ->setMethods(['getId', 'hasWishlistStoreId', 'getStoreId', 'getTypeInstance', 'getIsSalable']) ->getMock(); - $productMock->expects($this->once()) - ->method('getId') + $productMock->method('getId') ->willReturn($productId); - $productMock->expects($this->once()) - ->method('hasWishlistStoreId') + $productMock->method('hasWishlistStoreId') ->willReturn(false); - $productMock->expects($this->once()) - ->method('getStoreId') + $productMock->method('getStoreId') ->willReturn($storeId); - $productMock->expects($this->once()) - ->method('getTypeInstance') + $productMock->method('getTypeInstance') ->willReturn($instanceType); + $productMock->expects($this->any()) + ->method('getIsSalable') + ->willReturn($getIsSalable); $this->productRepository->expects($this->once()) ->method('getById') ->with($productId, false, $storeId) ->willReturn($productMock); - $this->serializer->expects($this->once()) - ->method('unserialize') + $this->serializer->method('unserialize') ->willReturnCallback( function ($value) { return json_decode($value, true); @@ -453,4 +470,17 @@ function ($value) { $this->assertEquals($result, $this->wishlist->addNewItem($productMock, $buyRequest)); } + + /** + * @return array[] + */ + public function addNewItemDataProvider(): array + { + return [ + [false, false, 'Cannot add product without stock to wishlist'], + [false, true, ''], + [true, false, ''], + [true, true, ''], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product.php index 22583483ddf69..7fe1983d3192f 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product.php @@ -5,7 +5,6 @@ */ declare(strict_types=1); - use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -30,6 +29,11 @@ $productRepository->cleanCache(); $product = $productRepository->get('product_disabled'); $wishlist->loadByCustomerId($customer->getId(), true); +/** @var \Magento\Catalog\Helper\Product $productHelper */ +$productHelper = $objectManager->get(\Magento\Catalog\Helper\Product::class); +$isSkipSaleableCheck = $productHelper->getSkipSaleableCheck(); +$productHelper->setSkipSaleableCheck(true); $item = $wishlist->addNewItem($product); +$productHelper->setSkipSaleableCheck($isSkipSaleableCheck); $wishlist->setSharingCode('wishlist_disabled_item'); $wishListResource->save($wishlist); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_simple_product.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_simple_product.php index 4961d2403672c..54c26b73c70ba 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_simple_product.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_simple_product.php @@ -25,4 +25,9 @@ $wishlistFactory = $objectManager->get(WishlistFactory::class); $wishlist = $wishlistFactory->create(); $wishlist->loadByCustomerId($customer->getId(), true); +/** @var \Magento\Catalog\Helper\Product $productHelper */ +$productHelper = $objectManager->get(\Magento\Catalog\Helper\Product::class); +$isSkipSaleableCheck = $productHelper->getSkipSaleableCheck(); +$productHelper->setSkipSaleableCheck(true); $wishlist->addNewItem($product); +$productHelper->setSkipSaleableCheck($isSkipSaleableCheck); From 392fc3a4ca49f9c3fba7d8ecc7688d8f7ae77c1c Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Mon, 7 Sep 2020 13:38:11 -0500 Subject: [PATCH 14/24] MC-37321: Quote customer_is_guest = false --- app/code/Magento/Checkout/Model/Session.php | 5 ++++- .../Magento/Checkout/Model/SessionTest.php | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 618f745e77105..8ba6af2518057 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -291,6 +291,7 @@ public function getQuote() } } else { $quote->setIsCheckoutCart(true); + $quote->setCustomerIsGuest(true); $this->_eventManager->dispatch('checkout_quote_init', ['quote' => $quote]); } } @@ -382,8 +383,9 @@ public function loadCustomerQuote() if ($customerQuote->getId() && $this->getQuoteId() != $customerQuote->getId()) { if ($this->getQuoteId()) { + $quote = $this->getQuote()->setCustomerIsGuest(false); $this->quoteRepository->save( - $customerQuote->merge($this->getQuote())->collectTotals() + $customerQuote->merge($quote)->collectTotals() ); $newQuote = $this->quoteRepository->get($customerQuote->getId()); $this->quoteRepository->save( @@ -402,6 +404,7 @@ public function loadCustomerQuote() $this->getQuote()->getBillingAddress(); $this->getQuote()->getShippingAddress(); $this->getQuote()->setCustomer($this->_customerSession->getCustomerDataObject()) + ->setCustomerIsGuest(false) ->setTotalsCollectedFlag(false) ->collectTotals(); $this->quoteRepository->save($this->getQuote()); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php index 32968572b4ac8..5b47b8f51c228 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php @@ -199,6 +199,10 @@ public function testLoadCustomerQuoteCustomerWithoutQuote(): void $this->quote->getCustomerEmail(), 'Precondition failed: Customer data must not be set to quote' ); + $this->assertTrue( + $this->quote->getCustomerIsGuest(), + 'Precondition failed: Customer must be as guest in quote' + ); $customer = $this->customerRepository->getById(1); $this->customerSession->setCustomerDataObject($customer); $this->quote = $this->checkoutSession->getQuote(); @@ -244,6 +248,17 @@ public function testGetQuoteWithProductWithTierPrice(): void $this->assertEquals($tierPriceValue, $quoteProduct->getTierPrice(1)); } + /** + * Test covers case when quote is not yet initialized and customer is guest + * + * Expected result - quote object should be loaded with customer as guest + */ + public function testGetQuoteNotInitializedGuest() + { + $quote = $this->checkoutSession->getQuote(); + $this->assertTrue($quote->getCustomerIsGuest()); + } + /** * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php @@ -288,5 +303,9 @@ private function validateCustomerDataInQuote(CartInterface $quote): void $quote->getCustomerFirstname(), 'Customer first name was not set to Quote correctly.' ); + $this->assertFalse( + $quote->getCustomerIsGuest(), + 'Customer should not be as guest in Quote.' + ); } } From b0f065c9b978f4e6b4e5d61a852b0fa4eca9bd6f Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 10 Aug 2020 16:39:28 -0500 Subject: [PATCH 15/24] MC-35692: Admin Order place order spinner do not disappear - Fix unable to create order with zero subtotal and reward points in admin --- .../view/adminhtml/web/js/transparent.js | 15 +- .../adminhtml/web/order/create/scripts.js | 16 +- .../adminhtml/web/js/transparent.test.js | 84 ++++++ .../adminhtml/js/order/create/scripts.test.js | 260 ++++++++++++++++++ 4 files changed, 366 insertions(+), 9 deletions(-) create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/Payment/adminhtml/web/js/transparent.test.js create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js diff --git a/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js b/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js index 0ec866ee6a831..296d542f3adf6 100644 --- a/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js +++ b/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js @@ -52,28 +52,29 @@ define([ * @param {String} method */ _setPlaceOrderHandler: function (event, method) { + var $editForm = $(this.options.editFormSelector); + + $editForm.off('beforeSubmitOrder.' + this.options.gateway); + if (method === this.options.gateway) { - $(this.options.editFormSelector) - .off('submitOrder') - .on('submitOrder.' + this.options.gateway, this._placeOrderHandler.bind(this)); - } else { - $(this.options.editFormSelector) - .off('submitOrder.' + this.options.gateway); + $editForm.on('beforeSubmitOrder.' + this.options.gateway, this._placeOrderHandler.bind(this)); } }, /** * Handler for form submit to call gateway for credit card validation. * + * @param {Event} event * @return {Boolean} * @private */ - _placeOrderHandler: function () { + _placeOrderHandler: function (event) { if ($(this.options.editFormSelector).valid()) { this._orderSave(); } else { $('body').trigger('processStop'); } + event.stopImmediatePropagation(); return false; }, diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index bbdd6f8fe8437..a329524c58d41 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -481,6 +481,13 @@ define([ }, switchPaymentMethod: function(method){ + if (this.paymentMethod !== method) { + jQuery('#edit_form') + .off('submitOrder') + .on('submitOrder', function(){ + jQuery(this).trigger('realOrder'); + }); + } jQuery('#edit_form').trigger('changePaymentMethod', [method]); this.setPaymentMethod(method); var data = {}; @@ -1308,11 +1315,16 @@ define([ }, submit: function () { - var $editForm = jQuery('#edit_form'); + var $editForm = jQuery('#edit_form'), + beforeSubmitOrderEvent; if ($editForm.valid()) { $editForm.trigger('processStart'); - $editForm.trigger('submitOrder'); + beforeSubmitOrderEvent = jQuery.Event('beforeSubmitOrder'); + $editForm.trigger(beforeSubmitOrderEvent); + if (beforeSubmitOrderEvent.result !== false) { + $editForm.trigger('submitOrder'); + } } }, diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Payment/adminhtml/web/js/transparent.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Payment/adminhtml/web/js/transparent.test.js new file mode 100644 index 0000000000000..4902fbad26ff3 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Payment/adminhtml/web/js/transparent.test.js @@ -0,0 +1,84 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*eslint-disable max-nested-callbacks*/ +/*jscs:disable jsDoc*/ +define([ + 'jquery', + 'jquery/validate', + 'Magento_Payment/js/transparent' +], function ($) { + 'use strict'; + + var containerEl, + formEl, + jQueryAjax; + + function init(config) { + var defaultConfig = { + orderSaveUrl: '/', + gateway: 'payflowpro', + editFormSelector: '#' + formEl.id + }; + + $(formEl).find(':radio[value="payflowpro"]').prop('checked', 'checked'); + $(formEl).transparent($.extend({}, defaultConfig, config || {})); + } + + beforeEach(function () { + if (!window.FORM_KEY) { + window.FORM_KEY = '61d0c9da0aa473d214f61913967cc0ea'; + } + $('<div id="admin_edit_order_form_container">' + + '<form id="admin_edit_order_form" action="/">' + + '<input type="radio" name="payment[method]" value="payflowpro"/>' + + '<input type="radio" name="payment[method]" value="money_order"/>' + + '</form>' + + '</div>' + ).appendTo(document.body); + containerEl = document.getElementById('admin_edit_order_form_container'); + formEl = document.getElementById('admin_edit_order_form'); + jQueryAjax = $.ajax; + }); + + afterEach(function () { + $(containerEl).remove(); + formEl = undefined; + containerEl = undefined; + $.ajax = jQueryAjax; + jQueryAjax = undefined; + }); + + describe('Magento_Payment/js/transparent', function () { + describe('beforeSubmitOrder handler', function () { + it('is registered when selected payment method requires transparent', function () { + init(); + expect(($._data(formEl, 'events') || {}).beforeSubmitOrder[0].type).toBe('beforeSubmitOrder'); + expect(($._data(formEl, 'events') || {}).beforeSubmitOrder[0].namespace).toBe('payflowpro'); + }); + it('is not registered when selected payment method does not require transparent', function () { + init(); + $(formEl).find(':radio[value="money_order"]').prop('checked', 'checked'); + $(formEl).trigger('changePaymentMethod', ['money_order']); + expect(($._data(formEl, 'events') || {}).beforeSubmitOrder).toBeUndefined(); + }); + it('returns false to prevent normal order creation', function () { + var beforeSubmitOrderEvent; + + $.ajax = jasmine.createSpy(); + init({ + orderSaveUrl: '/admin/paypal/transparent/requestSecureToken' + }); + beforeSubmitOrderEvent = $.Event('beforeSubmitOrder'); + $(formEl).trigger(beforeSubmitOrderEvent); + expect($.ajax).toHaveBeenCalledWith(jasmine.objectContaining({ + url: '/admin/paypal/transparent/requestSecureToken', + type: 'post' + })); + expect(beforeSubmitOrderEvent.result).toBe(false); + expect(beforeSubmitOrderEvent.isImmediatePropagationStopped()).toBe(true); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js new file mode 100644 index 0000000000000..0071d5af7df4e --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js @@ -0,0 +1,260 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*eslint-disable max-nested-callbacks*/ +/*jscs:disable jsDoc*/ +define([ + 'jquery', + 'squire', + 'jquery/validate' +], function ($, Squire) { + 'use strict'; + + var formEl, + jQueryAjax, + order, + tmpl = '<form id="edit_form" action="/">' + + '<section id="order-methods">' + + '<div id="order-billing_method"></div>' + + '<div id="order-shipping_method"></div>' + + '</section>' + + '<div id="order-billing_method_form">' + + '<input id="p_method_payment1" type="radio" name="payment[method]" value="payment1"/>' + + '<fieldset id="payment_form_payment1">' + + '<input type="number" name="payment[cc_number]"/>' + + '<input type="number" name="payment[cc_cid]"/>' + + '</fieldset>' + + '<input id="p_method_payment2" type="radio" name="payment[method]" value="payment2"/>' + + '<fieldset id="payment_form_payment2">' + + '<input type="number" name="payment[cc_number]"/>' + + '<input type="number" name="payment[cc_cid]"/>' + + '</fieldset>' + + '<input id="p_method_free" type="radio" name="payment[method]" value="free"/>' + + '</div>' + + '</form>'; + + $.widget('magetest.testPaymentMethodA', { + options: { + code: null, + orderSaveUrl: null, + orderFormSelector: null + }, + + _create: function () { + var $editForm = $(this.options.orderFormSelector); + + $editForm.off('changePaymentMethod.' + this.options.code) + .on('changePaymentMethod.' + this.options.code, this._onChangePaymentMethod.bind(this)); + }, + + _onChangePaymentMethod: function (event, method) { + var $editForm = $(this.options.orderFormSelector); + + $editForm.off('beforeSubmitOrder.' + this.options.code); + + if (method === this.options.code) { + $editForm.on('beforeSubmitOrder.' + this.options.code, this._submitOrder.bind(this)); + } + }, + + _submitOrder: function (event) { + $.ajax({ + url: this.options.orderSaveUrl, + type: 'POST', + context: this, + data: { + code: this.options.code + }, + dataType: 'JSON' + }); + event.stopImmediatePropagation(); + + return false; + } + + }); + + $.widget('magetest.testPaymentMethodB', $.magetest.testPaymentMethodA, { + isActive: false, + _onChangePaymentMethod: function (event, method) { + var $editForm = $(this.options.orderFormSelector), + isActive = method === this.options.code; + + if (this.isActive !== isActive) { + this.isActive = isActive; + + if (!isActive) { + $editForm.off('submitOrder.' + this.options.code); + } else { + $editForm.off('submitOrder') + .on('submitOrder.' + this.options.code, this._submitOrder.bind(this)); + } + } + } + }); + + function init(config) { + config = config || {}; + order = new window.AdminOrder({}); + $(formEl).validate({}); + $(formEl).find(':radio[value="payment1"]').testPaymentMethodA({ + code: 'payment1', + orderSaveUrl: '/admin/sales/order/create/payment_method/payment1', + orderFormSelector: '#' + formEl.id + }); + $(formEl).find(':radio[value="payment2"]').testPaymentMethodB({ + code: 'payment2', + orderSaveUrl: '/admin/sales/order/create/payment_method/payment2', + orderFormSelector: '#' + formEl.id + }); + $(formEl).off('realOrder').on('realOrder', function () { + $.ajax({ + url: '/admin/sales/order/create', + type: 'POST', + context: this, + data: $(this).serializeArray(), + dataType: 'JSON' + }); + }); + + if (config.method) { + $(formEl).find(':radio[value="' + config.method + '"]').prop('checked', true); + order.switchPaymentMethod(config.method); + } + } + + describe('Magento_Sales/order/create/scripts', function () { + var injector = new Squire(), + mocks = { + 'jquery': $, + 'Magento_Catalog/catalog/product/composite/configure': jasmine.createSpy(), + 'Magento_Ui/js/modal/confirm': jasmine.createSpy(), + 'Magento_Ui/js/modal/alert': jasmine.createSpy(), + 'Magento_Ui/js/lib/view/utils/async': jasmine.createSpy() + }; + + beforeEach(function (done) { + jQueryAjax = $.ajax; + injector.mock(mocks); + injector.require(['Magento_Sales/order/create/scripts'], function () { + window.FORM_KEY = window.FORM_KEY || '61d0c9da0aa473d214f61913967cc0ea'; + $(tmpl).appendTo(document.body); + formEl = document.getElementById('edit_form'); + $(formEl).off(); + done(); + }); + }); + + afterEach(function () { + try { + injector.clean(); + injector.remove(); + } catch (e) { + } + $(formEl).off().remove(); + formEl = undefined; + order = undefined; + $.ajax = jQueryAjax; + jQueryAjax = undefined; + }); + + describe('submit()', function () { + function testSubmit(currentPaymentMethod, paymentMethod, ajaxParams) { + $.ajax = jasmine.createSpy('$.ajax'); + init({ + method: currentPaymentMethod + }); + $(formEl).find(':radio[value="' + paymentMethod + '"]').prop('checked', true); + order.switchPaymentMethod(paymentMethod); + order.submit(); + expect($.ajax).toHaveBeenCalledTimes(1); + expect($.ajax).toHaveBeenCalledWith(jasmine.objectContaining(ajaxParams)); + } + + it('Check that payment custom handler is executed #1', function () { + testSubmit( + null, + 'payment1', + { + url: '/admin/sales/order/create/payment_method/payment1', + data: { + code: 'payment1' + } + } + ); + }); + + it('Check that payment custom handler is executed #2', function () { + testSubmit( + 'payment1', + 'payment1', + { + url: '/admin/sales/order/create/payment_method/payment1', + data: { + code: 'payment1' + } + } + ); + }); + + it('Check that payment custom handler is executed #3', function () { + testSubmit( + null, + 'payment2', + { + url: '/admin/sales/order/create/payment_method/payment2', + data: { + code: 'payment2' + } + } + ); + }); + + it('Check that payment custom handler is executed #4', function () { + testSubmit( + 'payment2', + 'payment2', + { + url: '/admin/sales/order/create/payment_method/payment2', + data: { + code: 'payment2' + } + } + ); + }); + + it('Check that native handler is executed for payment without custom handler #1', function () { + testSubmit( + 'payment1', + 'free', + { + url: '/admin/sales/order/create', + data: [ + { + name: 'payment[method]', + value: 'free' + } + ] + } + ); + }); + + it('Check that native handler is executed for payment without custom handler #2', function () { + testSubmit( + 'payment2', + 'free', + { + url: '/admin/sales/order/create', + data: [ + { + name: 'payment[method]', + value: 'free' + } + ] + } + ); + }); + }); + }); +}); From 5ab1f14706e098548eb71d9a1b09de25b2cf8aba Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Tue, 8 Sep 2020 11:12:02 -0500 Subject: [PATCH 16/24] MC-37321: Quote customer_is_guest = false --- app/code/Magento/Quote/Model/QuoteManagement.php | 1 + .../Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 3a81341e2b02a..c9c814fcaf52a 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -250,6 +250,7 @@ public function createEmptyCart() $quote->setBillingAddress($this->quoteAddressFactory->create()); $quote->setShippingAddress($this->quoteAddressFactory->create()); + $quote->setCustomerIsGuest(true); try { $quote->getShippingAddress()->setCollectShippingRates(true); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php index be183fe93815a..6f8d45aca5be4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php @@ -61,6 +61,7 @@ public function testCreateEmptyCart() self::assertNotNull($guestCart->getId()); self::assertNull($guestCart->getCustomer()->getId()); self::assertEquals('default', $guestCart->getStore()->getCode()); + self::assertTrue($guestCart->getCustomerIsGuest()); } /** @@ -81,6 +82,7 @@ public function testCreateEmptyCartWithNotDefaultStore() self::assertNotNull($guestCart->getId()); self::assertNull($guestCart->getCustomer()->getId()); self::assertSame('fixture_second_store', $guestCart->getStore()->getCode()); + self::assertTrue($guestCart->getCustomerIsGuest()); } /** From 03db1d60e9ea46144d06899745cb0a44bbb5626d Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Tue, 8 Sep 2020 11:13:39 -0500 Subject: [PATCH 17/24] MC-37321: Quote customer_is_guest = false --- app/code/Magento/Checkout/Model/Session.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 8ba6af2518057..22135e5528f97 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -383,7 +383,8 @@ public function loadCustomerQuote() if ($customerQuote->getId() && $this->getQuoteId() != $customerQuote->getId()) { if ($this->getQuoteId()) { - $quote = $this->getQuote()->setCustomerIsGuest(false); + $quote = $this->getQuote(); + $quote->setCustomerIsGuest(false); $this->quoteRepository->save( $customerQuote->merge($quote)->collectTotals() ); From 4ecc5d2eb3b55c61fd4f013579da0232a79bd900 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 8 Sep 2020 13:05:38 -0500 Subject: [PATCH 18/24] MC-36942: HTML minification strips triple slashes from html string in phtml --- .../Magento/Framework/View/Template/Html/Minifier.php | 2 +- .../Framework/View/Test/Unit/Template/Html/MinifierTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/Magento/Framework/View/Template/Html/Minifier.php b/lib/internal/Magento/Framework/View/Template/Html/Minifier.php index dba81a1547c44..c6ac7917871b0 100644 --- a/lib/internal/Magento/Framework/View/Template/Html/Minifier.php +++ b/lib/internal/Magento/Framework/View/Template/Html/Minifier.php @@ -144,7 +144,7 @@ function ($match) use (&$heredocs) { '#(?<!:|\\\\|\'|"|/)//(?!/)(?!\s*\<\!\[)(?!\s*]]\>)[^\n\r]*#', '', preg_replace( - '#(?<!:|\'|")//[^\n\r\<\?]*(\?\>)#', + '#(?<!:|\'|")//[^\n\r\<]*(\?\>)#', ' $1', preg_replace( '#(?<!:)//[^\n\r]*(\<\?php)[^\n\r]*(\s\?\>)[^\n\r]*#', diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Template/Html/MinifierTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Template/Html/MinifierTest.php index 3b13a2f723617..bb6edcd3fa0ff 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Template/Html/MinifierTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Template/Html/MinifierTest.php @@ -139,7 +139,7 @@ public function testMinify() <body> <a href="http://somelink.com/text.html">Text Link</a> <img src="test.png" alt="some text" /> - <?php echo \$block->someMethod(); ?> + <img src="data:image/gif;base64,P///yH5BAEAAAA" data-component="main-image"><?= \$block->someMethod(); ?> <div style="width: 800px" class="<?php echo \$block->getClass() ?>" /> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-component="main-image"> <script> @@ -182,7 +182,7 @@ public function testMinify() TEXT; $expectedContent = <<<TEXT -<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ ?> <?php ?> <html><head><title>Test titleText Link some textsomeMethod(); ?>