From fa0e9d31284d22de3e16d6fff635965218e8d399 Mon Sep 17 00:00:00 2001 From: Dmytro Yushkin Date: Wed, 28 Feb 2024 11:23:00 -0600 Subject: [PATCH 01/13] ACP2E-2785: Product image is lost after deleting an existing Scheduled Update that doesn't affect the image --- .../MediaImageDeleteProcessor.php | 2 +- .../MediaImageDeleteProcessorTest.php | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessorTest.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php index f49ddef01ca74..cfe9fb2cceb07 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php @@ -101,7 +101,7 @@ public function execute(DataObject $product): void */ private function canDeleteImage(string $file): bool { - return $this->productGallery->countImageUses($file) <= 1; + return $this->productGallery->countImageUses($file) < 1; } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessorTest.php new file mode 100644 index 0000000000000..11ea91fe0e642 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessorTest.php @@ -0,0 +1,95 @@ +mediaImageDeleteProcessor = $om->get(MediaImageDeleteProcessor::class); + $this->productRepository = $om->get(ProductRepositoryInterface::class); + + $this->mediaDirectory = $om->get(Filesystem::class)->getDirectoryRead(DirectoryList::MEDIA); + $this->config = $om->get(Config::class); + } + + #[ + DataFixture( + ProductFixture::class, + [ + 'sku' => 'simple', + 'media_gallery_entries' => [ + [] + ] + ] + ), + ] + public function testOnlyImageFileDeleted() + { + $product = $this->productRepository->get('simple'); + $image = $product->getMediaGalleryEntries()[0]; + $imageFilePath = $this->config->getBaseMediaPath() . $image['file']; + + $this->assertTrue( + $this->mediaDirectory->isExist($imageFilePath), + 'The image file not existed.' + ); + $this->mediaImageDeleteProcessor->execute($product); + $this->assertFalse( + $this->mediaDirectory->isExist($imageFilePath), + 'The image file must be deleted.' + ); + } +} From 605aedc23095ecbe32a755899179bf3e8483c52f Mon Sep 17 00:00:00 2001 From: arnsaha Date: Mon, 4 Mar 2024 10:29:23 -0600 Subject: [PATCH 02/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution with test coverage --- ...oryAndProductResolverOnSingleStoreMode.php | 120 ++++++++++++++++ ...logDataToWebsiteScopeOnSingleStoreMode.php | 84 +++++++++++ app/code/Magento/Catalog/etc/events.xml | 3 + ...ataToWebsiteScopeOnSingleStoreModeTest.php | 131 ++++++++++++++++++ 4 files changed, 338 insertions(+) create mode 100644 app/code/Magento/Catalog/Model/Product/CatalogCategoryAndProductResolverOnSingleStoreMode.php create mode 100644 app/code/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest.php diff --git a/app/code/Magento/Catalog/Model/Product/CatalogCategoryAndProductResolverOnSingleStoreMode.php b/app/code/Magento/Catalog/Model/Product/CatalogCategoryAndProductResolverOnSingleStoreMode.php new file mode 100644 index 0000000000000..5d1363e1328fd --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/CatalogCategoryAndProductResolverOnSingleStoreMode.php @@ -0,0 +1,120 @@ +resourceConnection->getConnection(); + $catalogProductTable = $this->resourceConnection->getTableName($table); + $select = $connection->select() + ->from($catalogProductTable, ['value_id', 'attribute_id', 'row_id']) + ->where('store_id = ?', $storeId); + $catalogProducts = $connection->fetchAll($select); + try { + if ($catalogProducts) { + foreach ($catalogProducts as $catalogProduct) { + $connection->delete( + $table, + [ + 'store_id = ?' => Store::DEFAULT_STORE_ID, + 'attribute_id = ?' => $catalogProduct['attribute_id'], + 'row_id = ?' => $catalogProduct['row_id'] + ] + ); + $connection->update( + $table, + ['store_id' => Store::DEFAULT_STORE_ID], + ['value_id = ?' => $catalogProduct['value_id']] + ); + } + } + } catch (LocalizedException $e) { + throw new CouldNotSaveException( + __($e->getMessage()), + $e + ); + } + } + + /** + * Migrate catalog category and product tables + * + * @param int $storeId + * @throws Exception + */ + public function migrateCatalogCategoryAndProductTables(int $storeId): void + { + $connection = $this->resourceConnection->getConnection(); + $tables = [ + 'catalog_category_entity_datetime', + 'catalog_category_entity_decimal', + 'catalog_category_entity_int', + 'catalog_category_entity_text', + 'catalog_category_entity_varchar', + 'catalog_product_entity_datetime', + 'catalog_product_entity_decimal', + 'catalog_product_entity_gallery', + 'catalog_product_entity_int', + 'catalog_product_entity_text', + 'catalog_product_entity_varchar', + ]; + try { + $connection->beginTransaction(); + foreach ($tables as $table) { + $this->process($storeId, $table); + } + $connection->commit(); + } catch (Exception $exception) { + $connection->rollBack(); + } + } +} diff --git a/app/code/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php b/app/code/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php new file mode 100644 index 0000000000000..d9848f72c4c9e --- /dev/null +++ b/app/code/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php @@ -0,0 +1,84 @@ +getEvent()->getChangedPaths(); + if (in_array(StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED, $changedPaths, true) + && $this->scopeConfig->getValue(StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED) + ) { + $store = $this->storeManager->getDefaultStoreView(); + if ($store) { + $storeId = $store->getId(); + $this->categoryAndProductResolver->migrateCatalogCategoryAndProductTables((int) $storeId); + $this->invalidateIndexer(); + } + } + } + + /** + * Invalidate related indexer + */ + private function invalidateIndexer(): void + { + $productIndexer = $this->indexerRegistry->get(Product::INDEXER_ID); + $categoryProductIndexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); + $priceIndexer = $this->indexerRegistry->get(PriceIndexProcessor::INDEXER_ID); + $ruleIndexer = $this->indexerRegistry->get(RuleProductProcessor::INDEXER_ID); + $productIndexer->invalidate(); + $categoryProductIndexer->invalidate(); + $priceIndexer->invalidate(); + $ruleIndexer->invalidate(); + } +} diff --git a/app/code/Magento/Catalog/etc/events.xml b/app/code/Magento/Catalog/etc/events.xml index 24186146c56f0..ee5643e9ddb11 100644 --- a/app/code/Magento/Catalog/etc/events.xml +++ b/app/code/Magento/Catalog/etc/events.xml @@ -67,4 +67,7 @@ + + + diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest.php new file mode 100644 index 0000000000000..7f9e7a75b9699 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest.php @@ -0,0 +1,131 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage(); + $this->config = $this->objectManager->get(ConfigInterface::class); + parent::setUp(); + } + + /** + * Test class for checking migration of product from store level scope to website scope in + * single store mode. + */ + #[ + DbIsolation(true), + DataFixture(CategoryFixture::class, ['name' => 'Category1', 'parent_id' => '2'], 'c11'), + DataFixture( + ProductFixture::class, + [ + 'sku' => 'simple_product', + 'name' => 'simple product for all store view', + 'price' => 35, + 'website_ids' => [1], + 'category_ids' => ['$c11.id$'] + ], + 'simple product for all store view' + ), + AppArea('adminhtml') + ] + public function testExecute(): void + { + $eventManager = $this->objectManager->get(ManagerInterface::class); + $scopeConfig = $this->objectManager->get(ReinitableConfigInterface::class); + $productFromFixture = $this->fixtures->get('simple product for all store view'); + + $product = $this->productRepository->get($productFromFixture->getSku()); + $this->assertEquals($productFromFixture->getName(), $product->getName()); + $this->assertEquals($productFromFixture->getPrice(), $product->getPrice()); + + $eventManager->dispatch( + 'admin_system_config_changed_section_general', + [ + 'website' => '', + 'store' => '', + 'changed_paths' => [ + StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED + ], + ] + ); + + $product->setName('simple product for default store view')->setStoreId(0); + $this->productRepository->save($product); + + $product = $this->productRepository->get($productFromFixture->getSku()); + $this->assertEquals('simple product for default store view', $product->getName()); + + $this->config->saveConfig('StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED', 1); + $scopeConfig->reinit(); + + $product = $this->productRepository->get($productFromFixture->getSku()); + $this->assertEquals('simple product for default store view', $product->getName()); + $this->assertEquals($productFromFixture->getPrice(), $product->getPrice()); + + $this->config->saveConfig('StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED', 0); + $scopeConfig->reinit(); + } +} From e9bd3a298497f0495701e217be312ac7828d7bfe Mon Sep 17 00:00:00 2001 From: arnsaha Date: Thu, 7 Mar 2024 09:17:53 -0600 Subject: [PATCH 03/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution without test coverage --- ...ataToWebsiteScopeOnSingleStoreModeTest.php | 131 ------------------ 1 file changed, 131 deletions(-) delete mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest.php deleted file mode 100644 index 7f9e7a75b9699..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest.php +++ /dev/null @@ -1,131 +0,0 @@ -objectManager = Bootstrap::getObjectManager(); - $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - $this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage(); - $this->config = $this->objectManager->get(ConfigInterface::class); - parent::setUp(); - } - - /** - * Test class for checking migration of product from store level scope to website scope in - * single store mode. - */ - #[ - DbIsolation(true), - DataFixture(CategoryFixture::class, ['name' => 'Category1', 'parent_id' => '2'], 'c11'), - DataFixture( - ProductFixture::class, - [ - 'sku' => 'simple_product', - 'name' => 'simple product for all store view', - 'price' => 35, - 'website_ids' => [1], - 'category_ids' => ['$c11.id$'] - ], - 'simple product for all store view' - ), - AppArea('adminhtml') - ] - public function testExecute(): void - { - $eventManager = $this->objectManager->get(ManagerInterface::class); - $scopeConfig = $this->objectManager->get(ReinitableConfigInterface::class); - $productFromFixture = $this->fixtures->get('simple product for all store view'); - - $product = $this->productRepository->get($productFromFixture->getSku()); - $this->assertEquals($productFromFixture->getName(), $product->getName()); - $this->assertEquals($productFromFixture->getPrice(), $product->getPrice()); - - $eventManager->dispatch( - 'admin_system_config_changed_section_general', - [ - 'website' => '', - 'store' => '', - 'changed_paths' => [ - StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED - ], - ] - ); - - $product->setName('simple product for default store view')->setStoreId(0); - $this->productRepository->save($product); - - $product = $this->productRepository->get($productFromFixture->getSku()); - $this->assertEquals('simple product for default store view', $product->getName()); - - $this->config->saveConfig('StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED', 1); - $scopeConfig->reinit(); - - $product = $this->productRepository->get($productFromFixture->getSku()); - $this->assertEquals('simple product for default store view', $product->getName()); - $this->assertEquals($productFromFixture->getPrice(), $product->getPrice()); - - $this->config->saveConfig('StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED', 0); - $scopeConfig->reinit(); - } -} From adb7c0b6ad7d2c0872b3c82220c25d2d04e4e4aa Mon Sep 17 00:00:00 2001 From: arnsaha Date: Mon, 11 Mar 2024 15:25:33 -0500 Subject: [PATCH 04/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution with test coverage --- ...oryAndProductResolverOnSingleStoreMode.php | 8 +- ...logDataToWebsiteScopeOnSingleStoreMode.php | 4 +- ...ndProductResolverOnSingleStoreModeTest.php | 130 ++++++++++++++++++ 3 files changed, 133 insertions(+), 9 deletions(-) rename app/code/Magento/Catalog/Model/{Product => ResourceModel}/CatalogCategoryAndProductResolverOnSingleStoreMode.php (94%) create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php diff --git a/app/code/Magento/Catalog/Model/Product/CatalogCategoryAndProductResolverOnSingleStoreMode.php b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php similarity index 94% rename from app/code/Magento/Catalog/Model/Product/CatalogCategoryAndProductResolverOnSingleStoreMode.php rename to app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php index 5d1363e1328fd..7a83c3424ece4 100644 --- a/app/code/Magento/Catalog/Model/Product/CatalogCategoryAndProductResolverOnSingleStoreMode.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php @@ -1,8 +1,5 @@ resourceConnectionMock = $this->createMock(ResourceConnection::class); + + $this->model = $objectManager->getObject( + Resolver::class, + [ + 'resourceConnection' => $this->resourceConnectionMock + ] + ); + } + + /** + * Test Migrate catalog category and product tables without exception + */ + public function testCheckMigrateCatalogCategoryAndProductTablesWithoutException(): void + { + $catalogProducts = [ + [ + 'id' => 1, + 'name' => 'simple1', + 'category_id' => '1', + 'website_id' => '2' + ], + [ + 'id' => 2, + 'name' => 'simple2', + 'category_id' => '1', + 'website_id' => '2' + ], + [ + 'id' => 3, + 'name' => 'bundle1', + 'category_id' => '1', + 'website_id' => '2' + ] + ]; + $connection = $this->getConnection(); + $connection->expects($this->any())->method('fetchCol')->willReturn($catalogProducts); + $connection->expects($this->any())->method('delete')->willReturnSelf(); + $connection->expects($this->any())->method('update')->willReturnSelf(); + $connection->expects($this->any())->method('commit')->willReturnSelf(); + + $this->model->migrateCatalogCategoryAndProductTables(1); + } + + /** + * Test Migrate catalog category and product tables with exception + */ + public function testCheckMigrateCatalogCategoryAndProductTablesWithException(): void + { + $exceptionMessage = 'Exception message'; + $connection = $this->getConnection(); + $connection->expects($this->any()) + ->method('fetchCol') + ->willThrowException(new Exception($exceptionMessage)); + $connection->expects($this->any())->method('rollBack')->willReturnSelf(); + $this->model->migrateCatalogCategoryAndProductTables(1); + } + + /** + * Get connection + * + * @return MockObject + */ + private function getConnection(): MockObject + { + $connection = $this->getMockForAbstractClass(AdapterInterface::class); + $this->resourceConnectionMock + ->expects($this->any()) + ->method('getConnection') + ->willReturn($connection); + $connection->expects($this->once()) + ->method('beginTransaction') + ->willReturnSelf(); + $this->resourceConnectionMock + ->expects($this->any()) + ->method('getTableName') + ->willReturnArgument(0); + + $select = $this->createMock(Select::class); + $select->expects($this->any())->method('from')->willReturnSelf(); + $select->expects($this->any())->method('where')->willReturnSelf(); + + $connection->expects($this->any())->method('select')->willReturn($select); + return $connection; + } +} From e0e5d8977289bdbb858dd62aa77ba4b2b60445a8 Mon Sep 17 00:00:00 2001 From: arnsaha Date: Mon, 18 Mar 2024 11:07:11 -0500 Subject: [PATCH 05/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution with test coverage --- ...oryAndProductResolverOnSingleStoreMode.php | 85 +++++++++++++++---- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php index 7a83c3424ece4..aee5f1f2128cf 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php @@ -47,29 +47,21 @@ public function __construct( */ private function process(int $storeId, string $table): void { - $connection = $this->resourceConnection->getConnection(); $catalogProductTable = $this->resourceConnection->getTableName($table); - $select = $connection->select() - ->from($catalogProductTable, ['value_id', 'attribute_id', 'row_id']) - ->where('store_id = ?', $storeId); - $catalogProducts = $connection->fetchAll($select); + + $catalogProducts = $this->getCatalogProducts($table, $storeId); + $rowIds = []; + $attributeIds = []; + $valueIds = []; try { if ($catalogProducts) { foreach ($catalogProducts as $catalogProduct) { - $connection->delete( - $table, - [ - 'store_id = ?' => Store::DEFAULT_STORE_ID, - 'attribute_id = ?' => $catalogProduct['attribute_id'], - 'row_id = ?' => $catalogProduct['row_id'] - ] - ); - $connection->update( - $table, - ['store_id' => Store::DEFAULT_STORE_ID], - ['value_id = ?' => $catalogProduct['value_id']] - ); + $rowIds[] = $catalogProduct['row_id']; + $attributeIds[] = $catalogProduct['attribute_id']; + $valueIds[] = $catalogProduct['value_id']; } + $this->massDelete($catalogProductTable, $attributeIds, $rowIds); + $this->massUpdate($catalogProductTable, $valueIds); } } catch (LocalizedException $e) { throw new CouldNotSaveException( @@ -111,4 +103,61 @@ public function migrateCatalogCategoryAndProductTables(int $storeId): void $connection->rollBack(); } } + + /** + * Delete default store related products + * + * @param $catalogProductTable + * @param array $attributeIds + * @param array $rowIds + * @return void + */ + private function massDelete($catalogProductTable, array $attributeIds, array $rowIds): void + { + $connection = $this->resourceConnection->getConnection(); + + $connection->delete( + $catalogProductTable, + [ + 'store_id = ?' => Store::DEFAULT_STORE_ID, + 'attribute_id IN(?)' => $attributeIds, + 'row_id IN(?)' => $rowIds + ] + ); + } + + /** + * Update default store related products + * + * @param $catalogProductTable + * @param array $valueIds + * @return void + */ + private function massUpdate($catalogProductTable, array $valueIds): void + { + $connection = $this->resourceConnection->getConnection(); + + $connection->update( + $catalogProductTable, + ['store_id' => Store::DEFAULT_STORE_ID], + ['value_id IN(?)' => $valueIds] + ); + } + + /** + * Get list of products + * + * @param string $table + * @param int $storeId + * @return array + */ + private function getCatalogProducts(string $table, int $storeId): array + { + $connection = $this->resourceConnection->getConnection(); + $catalogProductTable = $this->resourceConnection->getTableName($table); + $select = $connection->select() + ->from($catalogProductTable, ['value_id', 'attribute_id', 'row_id']) + ->where('store_id = ?', $storeId); + return $connection->fetchAll($select); + } } From f70273bc0984cd833f85359614039a9b387219ad Mon Sep 17 00:00:00 2001 From: arnsaha Date: Mon, 18 Mar 2024 12:58:20 -0500 Subject: [PATCH 06/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution with test coverage --- ...oryAndProductResolverOnSingleStoreMode.php | 21 +++++++---- ...ndProductResolverOnSingleStoreModeTest.php | 35 +++++++++++++------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php index aee5f1f2128cf..9e54f9fffb76b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php @@ -19,7 +19,10 @@ namespace Magento\Catalog\Model\ResourceModel; use Exception; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Eav\Api\Data\AttributeInterface; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\LocalizedException; use Magento\Store\Model\Store; @@ -31,9 +34,11 @@ class CatalogCategoryAndProductResolverOnSingleStoreMode { /** * @param ResourceConnection $resourceConnection + * @param MetadataPool $metadataPool */ public function __construct( - private readonly ResourceConnection $resourceConnection + private readonly ResourceConnection $resourceConnection, + private readonly MetadataPool $metadataPool ) { } @@ -47,6 +52,8 @@ public function __construct( */ private function process(int $storeId, string $table): void { + $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + $productLinkField = $productMetadata->getLinkField(); $catalogProductTable = $this->resourceConnection->getTableName($table); $catalogProducts = $this->getCatalogProducts($table, $storeId); @@ -56,8 +63,8 @@ private function process(int $storeId, string $table): void try { if ($catalogProducts) { foreach ($catalogProducts as $catalogProduct) { - $rowIds[] = $catalogProduct['row_id']; - $attributeIds[] = $catalogProduct['attribute_id']; + $rowIds[] = $catalogProduct[$productLinkField]; + $attributeIds[] = $catalogProduct[AttributeInterface::ATTRIBUTE_ID]; $valueIds[] = $catalogProduct['value_id']; } $this->massDelete($catalogProductTable, $attributeIds, $rowIds); @@ -107,12 +114,12 @@ public function migrateCatalogCategoryAndProductTables(int $storeId): void /** * Delete default store related products * - * @param $catalogProductTable + * @param string $catalogProductTable * @param array $attributeIds * @param array $rowIds * @return void */ - private function massDelete($catalogProductTable, array $attributeIds, array $rowIds): void + private function massDelete(string $catalogProductTable, array $attributeIds, array $rowIds): void { $connection = $this->resourceConnection->getConnection(); @@ -129,11 +136,11 @@ private function massDelete($catalogProductTable, array $attributeIds, array $ro /** * Update default store related products * - * @param $catalogProductTable + * @param string $catalogProductTable * @param array $valueIds * @return void */ - private function massUpdate($catalogProductTable, array $valueIds): void + private function massUpdate(string $catalogProductTable, array $valueIds): void { $connection = $this->resourceConnection->getConnection(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php index 56d9aa9d198ad..f4560add16c35 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php @@ -23,7 +23,8 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\EntityManager\MetadataPool; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -39,17 +40,21 @@ class CatalogCategoryAndProductResolverOnSingleStoreModeTest extends TestCase */ private $resourceConnectionMock; + /** + * @var MetadataPool|MockObject + */ + private $metadataPoolMock; + protected function setUp(): void { - $objectManager = new ObjectManager($this); $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); - - $this->model = $objectManager->getObject( - Resolver::class, - [ - 'resourceConnection' => $this->resourceConnectionMock - ] - ); + $this->metadataPoolMock = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->model = new Resolver( + $this->resourceConnectionMock, + $this->metadataPoolMock + ); } /** @@ -78,7 +83,7 @@ public function testCheckMigrateCatalogCategoryAndProductTablesWithoutException( ] ]; $connection = $this->getConnection(); - $connection->expects($this->any())->method('fetchCol')->willReturn($catalogProducts); + $connection->expects($this->any())->method('fetchAll')->willReturn($catalogProducts); $connection->expects($this->any())->method('delete')->willReturnSelf(); $connection->expects($this->any())->method('update')->willReturnSelf(); $connection->expects($this->any())->method('commit')->willReturnSelf(); @@ -94,7 +99,7 @@ public function testCheckMigrateCatalogCategoryAndProductTablesWithException(): $exceptionMessage = 'Exception message'; $connection = $this->getConnection(); $connection->expects($this->any()) - ->method('fetchCol') + ->method('fetchAll') ->willThrowException(new Exception($exceptionMessage)); $connection->expects($this->any())->method('rollBack')->willReturnSelf(); $this->model->migrateCatalogCategoryAndProductTables(1); @@ -108,6 +113,14 @@ public function testCheckMigrateCatalogCategoryAndProductTablesWithException(): private function getConnection(): MockObject { $connection = $this->getMockForAbstractClass(AdapterInterface::class); + $metadata = $this->getMockForAbstractClass(EntityMetadataInterface::class); + $this->metadataPoolMock->expects($this->any()) + ->method('getMetadata') + ->willReturn($metadata); + $metadata + ->expects($this->any()) + ->method('getLinkField') + ->willReturn('row_id'); $this->resourceConnectionMock ->expects($this->any()) ->method('getConnection') From 1639fb0f4ef56dd2b90f8f7e82954bc71e34ee7f Mon Sep 17 00:00:00 2001 From: arnsaha Date: Mon, 18 Mar 2024 15:59:44 -0500 Subject: [PATCH 07/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution with test coverage --- ...talogCategoryAndProductResolverOnSingleStoreModeTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php index f4560add16c35..9c6897ec063e7 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreModeTest.php @@ -52,9 +52,9 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); $this->model = new Resolver( - $this->resourceConnectionMock, - $this->metadataPoolMock - ); + $this->resourceConnectionMock, + $this->metadataPoolMock + ); } /** From 97504621f22aa01355d2454e40efaee713e67fe4 Mon Sep 17 00:00:00 2001 From: arnsaha Date: Tue, 19 Mar 2024 13:21:40 -0500 Subject: [PATCH 08/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution with test coverage --- ...oryAndProductResolverOnSingleStoreMode.php | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php index 9e54f9fffb76b..ea5817f4d4fe4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php @@ -52,22 +52,22 @@ public function __construct( */ private function process(int $storeId, string $table): void { - $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class); - $productLinkField = $productMetadata->getLinkField(); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); $catalogProductTable = $this->resourceConnection->getTableName($table); - $catalogProducts = $this->getCatalogProducts($table, $storeId); - $rowIds = []; + $catalogProducts = $this->getCatalogProducts($table, $linkField, $storeId); + $linkFieldIds = []; $attributeIds = []; $valueIds = []; try { if ($catalogProducts) { foreach ($catalogProducts as $catalogProduct) { - $rowIds[] = $catalogProduct[$productLinkField]; + $linkFieldIds[] = $catalogProduct[$linkField]; $attributeIds[] = $catalogProduct[AttributeInterface::ATTRIBUTE_ID]; $valueIds[] = $catalogProduct['value_id']; } - $this->massDelete($catalogProductTable, $attributeIds, $rowIds); + $this->massDelete($catalogProductTable, $linkField, $attributeIds, $linkFieldIds); $this->massUpdate($catalogProductTable, $valueIds); } } catch (LocalizedException $e) { @@ -115,11 +115,12 @@ public function migrateCatalogCategoryAndProductTables(int $storeId): void * Delete default store related products * * @param string $catalogProductTable + * @param string $linkField * @param array $attributeIds - * @param array $rowIds + * @param array $linkFieldIds * @return void */ - private function massDelete(string $catalogProductTable, array $attributeIds, array $rowIds): void + private function massDelete(string $catalogProductTable, string $linkField, array $attributeIds, array $linkFieldIds): void { $connection = $this->resourceConnection->getConnection(); @@ -127,8 +128,8 @@ private function massDelete(string $catalogProductTable, array $attributeIds, ar $catalogProductTable, [ 'store_id = ?' => Store::DEFAULT_STORE_ID, - 'attribute_id IN(?)' => $attributeIds, - 'row_id IN(?)' => $rowIds + AttributeInterface::ATTRIBUTE_ID. ' IN(?)' => $attributeIds, + $linkField.' IN(?)' => $linkFieldIds ] ); } @@ -155,15 +156,16 @@ private function massUpdate(string $catalogProductTable, array $valueIds): void * Get list of products * * @param string $table + * @param string $linkField * @param int $storeId * @return array */ - private function getCatalogProducts(string $table, int $storeId): array + private function getCatalogProducts(string $table, string $linkField, int $storeId): array { $connection = $this->resourceConnection->getConnection(); $catalogProductTable = $this->resourceConnection->getTableName($table); $select = $connection->select() - ->from($catalogProductTable, ['value_id', 'attribute_id', 'row_id']) + ->from($catalogProductTable, ['value_id', AttributeInterface::ATTRIBUTE_ID, $linkField]) ->where('store_id = ?', $storeId); return $connection->fetchAll($select); } From 290063660268f6070021b825bad1911acb49416e Mon Sep 17 00:00:00 2001 From: arnsaha Date: Tue, 19 Mar 2024 14:27:19 -0500 Subject: [PATCH 09/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution with test coverage --- ...CatalogCategoryAndProductResolverOnSingleStoreMode.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php index ea5817f4d4fe4..1ea43f033ec6b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/CatalogCategoryAndProductResolverOnSingleStoreMode.php @@ -120,8 +120,12 @@ public function migrateCatalogCategoryAndProductTables(int $storeId): void * @param array $linkFieldIds * @return void */ - private function massDelete(string $catalogProductTable, string $linkField, array $attributeIds, array $linkFieldIds): void - { + private function massDelete( + string $catalogProductTable, + string $linkField, + array $attributeIds, + array $linkFieldIds + ): void { $connection = $this->resourceConnection->getConnection(); $connection->delete( From 4a11b6ea270411106a93433cbe4c9d5b1ad3413a Mon Sep 17 00:00:00 2001 From: arnsaha Date: Tue, 19 Mar 2024 14:27:19 -0500 Subject: [PATCH 10/13] ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled - solution with test coverage --- .../MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php b/app/code/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php index 7bbc22a488ea9..886f09be3d8c7 100644 --- a/app/code/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php +++ b/app/code/Magento/Catalog/Observer/MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode.php @@ -57,6 +57,7 @@ public function execute(Observer $observer) $changedPaths = (array)$observer->getEvent()->getChangedPaths(); if (in_array(StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED, $changedPaths, true) && $this->scopeConfig->getValue(StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED) + && $this->storeManager->hasSingleStore() ) { $store = $this->storeManager->getDefaultStoreView(); if ($store) { From dfc54f95494cbee7b82fd8b3ae79aed88b39197f Mon Sep 17 00:00:00 2001 From: arnsaha Date: Fri, 22 Mar 2024 10:11:36 -0500 Subject: [PATCH 11/13] ACP2E-2949: [Cloud]Follow-up: Mismatch in Data Comparison when checking if data has changes - CHecking with test --- lib/internal/Magento/Framework/Model/AbstractModel.php | 10 ++++++---- .../Framework/Model/Test/Unit/AbstractModelTest.php | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/internal/Magento/Framework/Model/AbstractModel.php b/lib/internal/Magento/Framework/Model/AbstractModel.php index 5beef7c4136d0..118d825b16470 100644 --- a/lib/internal/Magento/Framework/Model/AbstractModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractModel.php @@ -1040,12 +1040,14 @@ public function getEventPrefix() private function checkAndConvertNumericValue(mixed $key, mixed $value): void { if (array_key_exists($key, $this->_data) && is_numeric($this->_data[$key]) - && $value != null + && $value !== null ) { - if (is_int($value)) { - $this->_data[$key] = (int) $this->_data[$key]; - } elseif (is_float($value)) { + if (is_float($value) || + (is_string($value) && preg_match('/^-?\d*\.\d+$/', $value)) + ) { $this->_data[$key] = (float) $this->_data[$key]; + } elseif (is_int($value)) { + $this->_data[$key] = (int) $this->_data[$key]; } } } diff --git a/lib/internal/Magento/Framework/Model/Test/Unit/AbstractModelTest.php b/lib/internal/Magento/Framework/Model/Test/Unit/AbstractModelTest.php index 250269b6ce942..891370232044b 100644 --- a/lib/internal/Magento/Framework/Model/Test/Unit/AbstractModelTest.php +++ b/lib/internal/Magento/Framework/Model/Test/Unit/AbstractModelTest.php @@ -233,6 +233,8 @@ public function getKeyValueDataPairs(): array 'when test data and compare data are float' => [['key' => 1.0], 'key', 1.0, false], 'when test data is 0 and compare data is null' => [['key' => 0], 'key', null, false], 'when test data is null and compare data is 0' => [['key' => null], 'key', 0, false], + 'when test data is string array and compare data is int' => [['key' => '10'], 'key', 10, false], + 'when test data is string array and compare data is float' => [['key' => '22.00'], 'key', 22.00, false] ]; } } From effd270e2cb194db14ed8246939de08669c77c15 Mon Sep 17 00:00:00 2001 From: arnsaha Date: Thu, 2 May 2024 18:27:45 -0500 Subject: [PATCH 12/13] ACP2E-3053: [Cloud] Elastic search error on certain category pages - Initial solution --- .../SearchAdapter/Aggregation/Interval.php | 10 ++++++ .../Magento/Elasticsearch/_files/requests.xml | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Aggregation/Interval.php b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Aggregation/Interval.php index 6f18a2d45d83d..be86d5b25c35e 100644 --- a/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Aggregation/Interval.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Aggregation/Interval.php @@ -99,6 +99,11 @@ public function load($limit, $offset = null, $lower = null, $upper = null) $to = ['lt' => $upper - self::DELTA]; } + if ($lower === null && $upper === null) { + $from = ['gte' => 0]; + $to = ['lt' => 0]; + } + $requestQuery = $this->prepareBaseRequestQuery($from, $to); $requestQuery = array_merge_recursive( $requestQuery, @@ -128,6 +133,11 @@ public function loadPrevious($data, $index, $lower = null) $to = ['lt' => $data - self::DELTA]; } + if ($lower === null && $data === 0.0) { + $from = ['gte' => 0]; + $to = ['lt' => 0]; + } + $requestQuery = $this->prepareBaseRequestQuery($from, $to); $requestQuery = array_merge_recursive( $requestQuery, diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/requests.xml b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/requests.xml index 0aaaf9b85857f..a2a586344d666 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/requests.xml +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/requests.xml @@ -56,6 +56,38 @@ 0 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 10 + From f63e109fec0a00727d22a007162b2b1eb81fddb6 Mon Sep 17 00:00:00 2001 From: soumah Date: Fri, 3 May 2024 12:49:38 -0500 Subject: [PATCH 13/13] ACP2E-3002: [CLOUD] Cannot Disable Send Emails from Admin UI as Dev Docs shows --- .../ApplyStoreEmailConfigToSalesEmail.php | 54 ++++++++++++ app/code/Magento/Sales/etc/di.xml | 4 + .../Mail/Template/TransportBuilderMock.php | 36 +++++++- .../Mail/TransportInterfaceMock.php | 18 ++-- .../Magento/Sales/Model/OrderTest.php | 88 +++++++++++++++++++ 5 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 app/code/Magento/Sales/Plugin/ApplyStoreEmailConfigToSalesEmail.php diff --git a/app/code/Magento/Sales/Plugin/ApplyStoreEmailConfigToSalesEmail.php b/app/code/Magento/Sales/Plugin/ApplyStoreEmailConfigToSalesEmail.php new file mode 100644 index 0000000000000..73227abf67213 --- /dev/null +++ b/app/code/Magento/Sales/Plugin/ApplyStoreEmailConfigToSalesEmail.php @@ -0,0 +1,54 @@ +scopeConfig->isSetFlag( + self::XML_PATH_SYSTEM_SMTP_DISABLE, + ScopeInterface::SCOPE_STORE, + $subject->getStore()->getStoreId() + ); + } +} diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index d5dc1938bdab5..639af0a2adf03 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -1038,4 +1038,8 @@ + + + diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php index cd9512c227893..d72fa78bc41a9 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php @@ -7,7 +7,7 @@ namespace Magento\TestFramework\Mail\Template; /** - * Class TransportBuilderMock + * Mock of mail transport builder */ class TransportBuilderMock extends \Magento\Framework\Mail\Template\TransportBuilder { @@ -16,6 +16,11 @@ class TransportBuilderMock extends \Magento\Framework\Mail\Template\TransportBui */ protected $_sentMessage; + /** + * @var callable + */ + private $onMessageSentCallback; + /** * Reset object state * @@ -24,7 +29,7 @@ class TransportBuilderMock extends \Magento\Framework\Mail\Template\TransportBui protected function reset() { $this->_sentMessage = $this->message; - parent::reset(); + return parent::reset(); } /** @@ -47,6 +52,31 @@ public function getTransport() { $this->prepareMessage(); $this->reset(); - return new \Magento\TestFramework\Mail\TransportInterfaceMock($this->message); + return $this->objectManager->create( + \Magento\TestFramework\Mail\TransportInterfaceMock::class, + [ + 'message' => $this->message, + 'onMessageSentCallback' => $this->onMessageSentCallback + ] + ); + } + + /** + * Set callback to be called when message is sent. + * + * @param callable $callback + */ + public function setOnMessageSentCallback(callable $callback): void + { + $this->onMessageSentCallback = $callback; + } + + /** + * Clean previous test data. + */ + public function clean(): void + { + $this->_sentMessage = null; + $this->onMessageSentCallback = null; } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php index 5bf98b76e7d59..05d5cda1a6baf 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php @@ -9,7 +9,7 @@ use Magento\Framework\Mail\EmailMessageInterface; /** - * Class TransportInterfaceMock + * Mock of mail transport interface */ class TransportInterfaceMock implements \Magento\Framework\Mail\TransportInterface { @@ -18,14 +18,23 @@ class TransportInterfaceMock implements \Magento\Framework\Mail\TransportInterfa */ private $message; + /** + * @var null|callable + */ + private $onMessageSentCallback; + /** * TransportInterfaceMock constructor. * * @param null|EmailMessageInterface $message + * @param null|callable $onMessageSentCallback */ - public function __construct($message = null) - { + public function __construct( + $message = null, + ?callable $onMessageSentCallback = null + ) { $this->message = $message; + $this->onMessageSentCallback = $onMessageSentCallback; } /** @@ -35,8 +44,7 @@ public function __construct($message = null) */ public function sendMessage() { - //phpcs:ignore Squiz.PHP.NonExecutableCode.ReturnNotRequired - return; + $this->onMessageSentCallback && call_user_func($this->onMessageSentCallback, $this->message); } /** diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php index 8952bde98e385..69242db7f5e7c 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php @@ -14,8 +14,12 @@ use Magento\Checkout\Test\Fixture\SetGuestEmail as SetGuestEmailFixture; use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture; use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture; +use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; +use Magento\Sales\Model\Order\Email\Container\OrderIdentity; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\Sales\Test\Fixture\Creditmemo as CreditmemoFixture; use Magento\Sales\Test\Fixture\Invoice as InvoiceFixture; use Magento\SalesRule\Model\Rule; @@ -25,8 +29,12 @@ use Magento\TestFramework\Fixture\DataFixtureStorage; use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class OrderTest extends TestCase { /** @@ -34,12 +42,52 @@ class OrderTest extends TestCase */ private $fixtures; + /** + * @var TransportBuilderMock + */ + private $transportBuilderMock; + + /** + * @var MutableScopeConfigInterface + */ + private $mutableScopeConfig; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var EmailSenderHandler + */ + private $emailSenderHandler; + /** * Set up */ protected function setUp(): void { $this->fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); + $objectManager = Bootstrap::getObjectManager(); + $this->collectionFactory = $objectManager->get(CollectionFactory::class); + $this->transportBuilderMock = $objectManager->get(TransportBuilderMock::class); + $this->mutableScopeConfig = $objectManager->get(MutableScopeConfigInterface::class); + $this->emailSenderHandler = Bootstrap::getObjectManager()->create( + EmailSenderHandler::class, + [ + 'emailSender' => $objectManager->get(OrderSender::class), + 'entityResource' => $objectManager->get(\Magento\Sales\Model\ResourceModel\Order::class), + 'entityCollection' => $this->collectionFactory->create(), + 'identityContainer' => $objectManager->create(OrderIdentity::class), + ] + ); + $this->transportBuilderMock->clean(); + } + + protected function tearDown(): void + { + $this->transportBuilderMock->clean(); + parent::tearDown(); } /** @@ -90,4 +138,44 @@ public function testMultipleCreditmemosForZeroTotalOrder() 'Should be possible to create second credit memo for zero total order if not all items are refunded yet' ); } + + #[ + Config('system/smtp/disable', '1', 'store', 'default'), + Config('sales_email/general/async_sending', '1'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddProductToCartFixture::class, + ['cart_id' => '$cart.id$', 'product_id' => '$product.id$'] + ), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order') + ] + public function testAsyncEmailForOrderCreatedWhenEmailSendingWasDisabled(): void + { + $isEmailSent = false; + $this->transportBuilderMock->setOnMessageSentCallback( + function () use (&$isEmailSent) { + $isEmailSent = true; + } + ); + $order = $this->fixtures->get('order'); + $this->assertEquals(0, $order->getSendEmail()); + $this->assertNull($order->getEmailSent()); + $this->mutableScopeConfig->setValue('system/smtp/disable', 0, 'store', 'default'); + $this->emailSenderHandler->sendEmails(); + $this->assertFalse( + $isEmailSent, + 'Email is not expected to be sent' + ); + $collection = $this->collectionFactory->create(); + $collection->addFieldToFilter('entity_id', $order->getId()); + $order = $collection->getFirstItem(); + $this->assertEquals(0, $order->getSendEmail()); + $this->assertNull($order->getEmailSent()); + } }