diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 007529a06d9f4..298b51d7fbac0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -263,4 +263,8 @@ EavStockItem CustomAttributeCategoryIds + + 1 + EavStock1 + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml index 0f6b383c3b743..5424e48261085 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml @@ -11,4 +11,7 @@ Qty_1000 + + Qty_1 + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml index 46a3fa3657f2c..99e072b91c3a9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml @@ -12,4 +12,8 @@ 1000 true + + 1 + true + diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php index a32faa4640a86..b3fa07479a712 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php @@ -10,8 +10,10 @@ use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\CacheContext; use Magento\CatalogInventory\Model\Stock; use Magento\Catalog\Model\Product; @@ -46,25 +48,35 @@ class CacheCleaner */ private $connection; + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @param ResourceConnection $resource * @param StockConfigurationInterface $stockConfiguration * @param CacheContext $cacheContext * @param ManagerInterface $eventManager + * @param MetadataPool|null $metadataPool */ public function __construct( ResourceConnection $resource, StockConfigurationInterface $stockConfiguration, CacheContext $cacheContext, - ManagerInterface $eventManager + ManagerInterface $eventManager, + MetadataPool $metadataPool = null ) { $this->resource = $resource; $this->stockConfiguration = $stockConfiguration; $this->cacheContext = $cacheContext; $this->eventManager = $eventManager; + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); } /** + * Clean cache by product ids. + * * @param array $productIds * @param callable $reindex * @return void @@ -76,22 +88,37 @@ public function clean(array $productIds, callable $reindex) $productStatusesAfter = $this->getProductStockStatuses($productIds); $productIds = $this->getProductIdsForCacheClean($productStatusesBefore, $productStatusesAfter); if ($productIds) { - $this->cacheContext->registerEntities(Product::CACHE_TAG, $productIds); + $this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds)); $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); } } /** + * Get current stock statuses for product ids. + * * @param array $productIds * @return array */ private function getProductStockStatuses(array $productIds) { + $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); $select = $this->getConnection()->select() ->from( - $this->resource->getTableName('cataloginventory_stock_status'), + ['css' => $this->resource->getTableName('cataloginventory_stock_status')], ['product_id', 'stock_status', 'qty'] - )->where('product_id IN (?)', $productIds) + ) + ->joinLeft( + ['cpr' => $this->resource->getTableName('catalog_product_relation')], + 'css.product_id = cpr.child_id', + [] + ) + ->joinLeft( + ['cpe' => $this->resource->getTableName('catalog_product_entity')], + 'cpr.parent_id = cpe.' . $linkField, + ['parent_id' => 'cpe.entity_id'] + ) + ->where('product_id IN (?)', $productIds) ->where('stock_id = ?', Stock::DEFAULT_STOCK_ID) ->where('website_id = ?', $this->stockConfiguration->getDefaultScopeId()); @@ -125,6 +152,9 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array if ($statusBefore['stock_status'] !== $statusAfter['stock_status'] || ($stockThresholdQty && $statusAfter['qty'] <= $stockThresholdQty)) { $productIds[] = $productId; + if (isset($statusAfter['parent_id'])) { + $productIds[] = $statusAfter['parent_id']; + } } } @@ -132,6 +162,8 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array } /** + * Get database connection. + * * @return AdapterInterface */ private function getConnection() diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml new file mode 100644 index 0000000000000..9a5ad027da600 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml @@ -0,0 +1,91 @@ + + + + + + + + + + <description value="After last configurable product was ordered it becomes out of stock"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96031"/> + <group value="CatalogInventory"/> + </annotations> + + <before> + <!--Create Configurable product--> + <actionGroup ref="AdminCreateConfigurableProductChildQty1ActionGroup" stepKey="createConfigurableProduct"> + <argument name="productName" value="ApiConfigurableProduct"/> + </actionGroup> + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> + <field key="group_id">1</field> + </createData> + <!--Update index mode, reindex, flush cache--> + <magentoCLI command="indexer:set-mode schedule" stepKey="setScheduleIndexMode"/> + <magentoCLI command="indexer:reindex" stepKey="reindexBefore"/> + <magentoCLI command="cache:flush" stepKey="cacheFlushBefore"/> + </before> + + <after> + <!--Delete configurable product--> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> + <!--Delete customer--> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + <!--Update index mode, reindex, flush cache--> + <magentoCLI command="indexer:set-mode realtime" stepKey="setRealTimeIndexMode"/> + <magentoCLI command="indexer:reindex" stepKey="reindexAfter"/> + <magentoCLI command="cache:flush" stepKey="cacheFlushAfter"/> + </after> + + <!-- Login as a customer --> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="signUpNewUser"> + <argument name="customer" value="$$createSimpleUsCustomer$$"/> + </actionGroup> + + <!-- Go to configurable product page --> + <amOnPage url="{{StorefrontProductPage.url('apiconfigurableproduct')}}" stepKey="goToConfigurableProductPage"/> + + <!-- Order product with single quantity --> + <selectOption userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct.option[store_labels][1][label]$$" + selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttributeCreateConfigurableProduct.attribute_id$$)}}" + stepKey="configProductFillOption" + /> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectShippingMehod"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeorder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + + <!--Run cron to reindex products--> + <magentoCLI command="cron:run --group='index'" stepKey="runCron"/> + <magentoCLI command="cron:run --group='index'" stepKey="runCron1"/> + + <!-- Go to configurable product page --> + <amOnPage url="{{StorefrontProductPage.url('apiconfigurableproduct')}}" stepKey="goToConfigurableProductPage1"/> + + <!-- Assert that ordered product with single quantity is not available for order --> + <dontSee userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct.option[store_labels][1][label]$$" + selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttributeCreateConfigurableProduct.attribute_id$$)}}" + stepKey="assertOptionNotAvailable" + /> + + <!-- Logout customer on Storefront--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutStorefront"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php index 5e4249685f8d3..a1282c45ce1fc 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php @@ -12,6 +12,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\CacheContext; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Catalog\Model\Product; @@ -43,6 +44,11 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase */ private $cacheContextMock; + /** + * @var MetadataPool |\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoolMock; + /** * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -61,6 +67,8 @@ protected function setUp() ->setMethods(['getStockThresholdQty'])->getMockForAbstractClass(); $this->cacheContextMock = $this->getMockBuilder(CacheContext::class)->disableOriginalConstructor()->getMock(); $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class)->getMock(); + $this->metadataPoolMock = $this->getMockBuilder(MetadataPool::class) + ->setMethods(['getMetadata', 'getLinkField'])->disableOriginalConstructor()->getMock(); $this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); $this->resourceMock->expects($this->any()) @@ -73,7 +81,8 @@ protected function setUp() 'resource' => $this->resourceMock, 'stockConfiguration' => $this->stockConfigurationMock, 'cacheContext' => $this->cacheContextMock, - 'eventManager' => $this->eventManagerMock + 'eventManager' => $this->eventManagerMock, + 'metadataPool' => $this->metadataPoolMock, ] ); } @@ -90,6 +99,7 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto $productId = 123; $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock); $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( [ @@ -105,7 +115,10 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto ->with(Product::CACHE_TAG, [$productId]); $this->eventManagerMock->expects($this->once())->method('dispatch') ->with('clean_cache_by_tags', ['object' => $this->cacheContextMock]); - + $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata') + ->willReturnSelf(); + $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField') + ->willReturn('row_id'); $callback = function () { }; $this->unit->clean([], $callback); @@ -136,6 +149,7 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft $productId = 123; $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock); $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( [ @@ -149,6 +163,10 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft ->willReturn($stockThresholdQty); $this->cacheContextMock->expects($this->never())->method('registerEntities'); $this->eventManagerMock->expects($this->never())->method('dispatch'); + $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata') + ->willReturnSelf(); + $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField') + ->willReturn('row_id'); $callback = function () { }; diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index db747dbdba657..aa168899ddb02 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> <!-- Checkout select Check/Money Order payment --> <actionGroup name="CheckoutSelectCheckMoneyOrderPaymentActionGroup"> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <conditionalClick selector="{{CheckoutPaymentSection.checkMoneyOrderPayment}}" dependentSelector="{{CheckoutPaymentSection.checkMoneyOrderPayment}}" visible="true" stepKey="clickCheckMoneyOrderPayment"/> </actionGroup> @@ -66,4 +67,10 @@ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> </actionGroup> + + <!-- Checkout select Flat Rate shipping method --> + <actionGroup name="CheckoutSelectFlatRateShippingMethodActionGroup"> + <conditionalClick selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" dependentSelector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" visible="true" stepKey="selectFlatRateShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForNextButton"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml index 75686d23a11b9..fafbe84fe568e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml @@ -62,4 +62,14 @@ <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> </actionGroup> + + <actionGroup name="AdminCreateConfigurableProductChildQty1ActionGroup" extends="AdminCreateApiConfigurableProductActionGroup"> + <createData entity="ApiConfigurableProductWithOutCategory" stepKey="createConfigProduct"> + <field key="name">{{productName}}</field> + </createData> + <createData entity="ApiSimpleSingleQty" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 996a392230eea..be0c2b05e48ba 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,5 +13,6 @@ <element name="productAttributeOptions" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> <element name="stockIndication" type="block" selector=".stock" /> <element name="productAttributeOptionsSelectButton" type="select" selector="#product-options-wrapper .super-attribute-select"/> + <element name="optionByAttributeId" type="input" selector="#attribute{{var1}}" parameterized="true"/> </section> </sections>