diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index f4f82d5a5c5b..142b7406cf2a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1,7 +1,7 @@ 'Imported resource (image: %s) at row %s could not be downloaded from external resource due to timeout or access permissions', ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid', ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually', + self::ERROR_DUPLICATE_URL_KEY_BY_CATEGORY => 'Url key: \'%s\' was already generated for a %s with the ID: %s. You need to specify the unique URL key manually', ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values', 'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date', // Can't add new translated strings in patch release @@ -933,7 +936,7 @@ public function __construct( $this->stockProcessor = $stockProcessor ?: ObjectManager::getInstance() ->get(StockProcessor::class); $this->linkProcessor = $linkProcessor ?? ObjectManager::getInstance() - ->get(LinkProcessor::class); + ->get(LinkProcessor::class); $this->linkProcessor->addNameToIds($this->_linkNameToId); $this->hashAlgorithm = (version_compare(PHP_VERSION, '8.1.0') >= 0) ? 'xxh128' : 'crc32c'; parent::__construct( @@ -949,7 +952,7 @@ public function __construct( $this->_optionEntity = $data['option_entity'] ?? $optionFactory->create(['data' => ['product_entity' => $this]]); $this->skuStorage = $skuStorage ?? ObjectManager::getInstance() - ->get(SkuStorage::class); + ->get(SkuStorage::class); $this->_initAttributeSets() ->_initTypeModels() ->_initSkus() @@ -957,9 +960,9 @@ public function __construct( $this->validator->init($this); $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class); $this->productRepository = $productRepository ?? ObjectManager::getInstance() - ->get(ProductRepositoryInterface::class); + ->get(ProductRepositoryInterface::class); $this->stockItemProcessor = $stockItemProcessor ?? ObjectManager::getInstance() - ->get(StockItemProcessorInterface::class); + ->get(StockItemProcessorInterface::class); $this->fileDriver = $fileDriver ?? ObjectManager::getInstance() ->get(File::class); } @@ -3117,7 +3120,7 @@ protected function _saveValidatedBunches() } /** - * Check that url_keys are not assigned to other products in DB + * Check that url_keys are not already assigned to others entities in DB * * @return void * @since 100.0.3 @@ -3129,7 +3132,11 @@ protected function checkUrlKeyDuplicates() $urlKeyDuplicates = $this->_connection->fetchAssoc( $this->_connection->select()->from( ['url_rewrite' => $resource->getTable('url_rewrite')], - ['request_path', 'store_id'] + [ + 'request_path', + 'store_id', + 'entity_type' + ] )->joinLeft( ['cpe' => $resource->getTable('catalog_product_entity')], "cpe.entity_id = url_rewrite.entity_id" @@ -3137,13 +3144,24 @@ protected function checkUrlKeyDuplicates() ->where('store_id IN (?)', $storeId) ->where('cpe.sku not in (?)', array_values($urlKeys)) ); + foreach ($urlKeyDuplicates as $entityData) { $rowNum = $this->rowNumbers[$entityData['store_id']][$entityData['request_path']]; - $message = sprintf( - $this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY), - $entityData['request_path'], - $entityData['sku'] - ); + if ($entityData['entity_type'] === 'category') { + $message = sprintf( + $this->retrieveMessageTemplate(self::ERROR_DUPLICATE_URL_KEY_BY_CATEGORY), + $entityData['request_path'], + $entityData['entity_type'], + $entityData['entity_id'], + ); + } else { + $message = sprintf( + $this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY), + $entityData['request_path'], + $entityData['sku'] + ); + } + $this->addRowError(ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, 'url_key', $message); } } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index de7a4852ca58..a2e3ebcf29b9 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -1,7 +1,7 @@ _initSkus() ->_initImagesArrayKeys(); - $objectManager = new ObjectManager($this); + $this->objectManager = new ObjectManager($this); + $this->productPropertiesMap = [ + 'jsonHelper' => $this->jsonHelper, + 'importExportData' => $this->importExportData, + 'importData' => $this->_dataSourceModel, + 'config' => $this->config, + 'resource' => $this->resource, + 'resourceHelper' => $this->resourceHelper, + 'string' => $this->string, + 'errorAggregator' => $this->errorAggregator, + 'eventManager' => $this->_eventManager, + 'stockRegistry' => $this->stockRegistry, + 'stockConfiguration' => $this->stockConfiguration, + 'stockStateProvider' => $this->stockStateProvider, + 'catalogData' => $this->_catalogData, + 'importConfig' => $this->_importConfig, + 'resourceFactory' => $this->_resourceFactory, + 'optionFactory' => $this->optionFactory, + 'setColFactory' => $this->_setColFactory, + 'productTypeFactory' => $this->_productTypeFactory, + 'linkFactory' => $this->_linkFactory, + 'proxyProdFactory' => $this->_proxyProdFactory, + 'uploaderFactory' => $this->_uploaderFactory, + 'filesystem' => $this->_filesystem, + 'stockResItemFac' => $this->_stockResItemFac, + 'localeDate' => $this->_localeDate, + 'dateTime' => $this->dateTime, + 'logger' => $this->_logger, + 'indexerRegistry' => $this->indexerRegistry, + 'storeResolver' => $this->storeResolver, + 'skuProcessor' => $this->skuProcessor, + 'categoryProcessor' => $this->categoryProcessor, + 'validator' => $this->validator, + 'objectRelationProcessor' => $this->objectRelationProcessor, + 'transactionManager' => $this->transactionManager, + 'taxClassProcessor' => $this->taxClassProcessor, + 'scopeConfig' => $this->scopeConfig, + 'productUrl' => $this->productUrl, + 'data' => $this->data, + 'imageTypeProcessor' => $this->imageTypeProcessor, + 'skuStorage' => $this->skuStorageMock, + ]; - $this->importProduct = $objectManager->getObject( + $this->importProduct = $this->objectManager->getObject( Product::class, - [ - 'jsonHelper' => $this->jsonHelper, - 'importExportData' => $this->importExportData, - 'importData' => $this->_dataSourceModel, - 'config' => $this->config, - 'resource' => $this->resource, - 'resourceHelper' => $this->resourceHelper, - 'string' => $this->string, - 'errorAggregator' => $this->errorAggregator, - 'eventManager' => $this->_eventManager, - 'stockRegistry' => $this->stockRegistry, - 'stockConfiguration' => $this->stockConfiguration, - 'stockStateProvider' => $this->stockStateProvider, - 'catalogData' => $this->_catalogData, - 'importConfig' => $this->_importConfig, - 'resourceFactory' => $this->_resourceFactory, - 'optionFactory' => $this->optionFactory, - 'setColFactory' => $this->_setColFactory, - 'productTypeFactory' => $this->_productTypeFactory, - 'linkFactory' => $this->_linkFactory, - 'proxyProdFactory' => $this->_proxyProdFactory, - 'uploaderFactory' => $this->_uploaderFactory, - 'filesystem' => $this->_filesystem, - 'stockResItemFac' => $this->_stockResItemFac, - 'localeDate' => $this->_localeDate, - 'dateTime' => $this->dateTime, - 'logger' => $this->_logger, - 'indexerRegistry' => $this->indexerRegistry, - 'storeResolver' => $this->storeResolver, - 'skuProcessor' => $this->skuProcessor, - 'categoryProcessor' => $this->categoryProcessor, - 'validator' => $this->validator, - 'objectRelationProcessor' => $this->objectRelationProcessor, - 'transactionManager' => $this->transactionManager, - 'taxClassProcessor' => $this->taxClassProcessor, - 'scopeConfig' => $this->scopeConfig, - 'productUrl' => $this->productUrl, - 'data' => $this->data, - 'imageTypeProcessor' => $this->imageTypeProcessor, - 'skuStorage' => $this->skuStorageMock - ] + $this->productPropertiesMap ); $reflection = new \ReflectionClass(Product::class); $reflectionProperty = $reflection->getProperty('metadataPool'); @@ -556,9 +567,9 @@ protected function _objectConstructor() $this->optionEntity = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() ->getMock(); - $this->optionFactory->expects($this->once())->method('create')->willReturn($this->optionEntity); + $this->optionFactory->expects($this->atLeastOnce())->method('create')->willReturn($this->optionEntity); - $this->_filesystem->expects($this->once()) + $this->_filesystem->expects($this->atLeastOnce()) ->method('getDirectoryWrite') ->with(DirectoryList::ROOT) ->willReturn($this->_mediaDirectory); @@ -581,7 +592,7 @@ protected function _parentObjectConstructor() $this->_connection = $this->getMockForAbstractClass(AdapterInterface::class); $this->select = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() - ->onlyMethods(['from', 'where']) + ->onlyMethods(['from', 'where', 'joinLeft']) ->getMock(); $this->select->expects($this->any())->method('from')->willReturnSelf(); //$this->select->expects($this->any())->method('where')->willReturnSelf(); @@ -617,11 +628,11 @@ protected function _initAttributeSets() $collection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); - $collection->expects($this->once()) + $collection->expects($this->atLeastOnce()) ->method('setEntityTypeFilter') ->with(self::ENTITY_TYPE_ID) ->willReturn($attributeSetCol); - $this->_setColFactory->expects($this->once()) + $this->_setColFactory->expects($this->atLeastOnce()) ->method('create') ->willReturn($collection); return $this; @@ -641,20 +652,20 @@ protected function _initTypeModels() $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMock(); - $productTypeInstance->expects($this->once()) + $productTypeInstance->expects($this->atLeastOnce()) ->method('isSuitable') ->willReturn(true); - $productTypeInstance->expects($this->once()) + $productTypeInstance->expects($this->atLeastOnce()) ->method('getParticularAttributes') ->willReturn([]); - $productTypeInstance->expects($this->once()) + $productTypeInstance->expects($this->atLeastOnce()) ->method('getCustomFieldsMapping') ->willReturn([]); - $this->_importConfig->expects($this->once()) + $this->_importConfig->expects($this->atLeastOnce()) ->method('getEntityTypes') ->with(self::ENTITY_TYPE_CODE) ->willReturn($entityTypes); - $this->_productTypeFactory->expects($this->once())->method('create')->willReturn($productTypeInstance); + $this->_productTypeFactory->expects($this->atLeastOnce())->method('create')->willReturn($productTypeInstance); return $this; } @@ -663,10 +674,8 @@ protected function _initTypeModels() */ protected function _initSkus() { - $this->skuProcessor->expects($this->once())->method('setTypeModels'); - $this->skuProcessor->expects($this->never())->method('reloadOldSkus')->willReturnSelf(); - $this->skuProcessor->expects($this->never())->method('getOldSkus')->willReturn([]); - $this->skuStorageMock->expects($this->once())->method('reset'); + $this->skuProcessor->expects($this->atLeastOnce())->method('setTypeModels'); + $this->skuStorageMock->expects($this->atLeastOnce())->method('reset'); return $this; } @@ -675,7 +684,7 @@ protected function _initSkus() */ protected function _initImagesArrayKeys() { - $this->imageTypeProcessor->expects($this->once())->method('getImageTypes')->willReturn( + $this->imageTypeProcessor->expects($this->atLeastOnce())->method('getImageTypes')->willReturn( ['image', 'small_image', 'thumbnail', 'swatch_image', '_media_image'] ); return $this; @@ -2273,4 +2282,194 @@ public function testGetRemoteFileContent() $property->invokeArgs($this->importProduct, ['php://filter']) ); } + + /** + * Test when import product throws an error when the file has duplicated Url Keys from another entity. + * + * @param array $dataProvider + * + * @dataProvider duplicatedUrlCheckDataProvider + * @return void + */ + public function testImportProductOnDuplicatedUrlKey(array $dataProvider): void + { + $tableName = $dataProvider['table_name']; + $tableNameProduct = $dataProvider['table_name_product']; + $callIndexTableName = 0; + $errorAggregator = $this->setUpPropertiesMap($dataProvider); + $importProduct = $this->objectManager->getObject( + Product::class, + $this->productPropertiesMap + ); + + $this->_resourceFactory->expects($this->once()) + ->method('create') + ->willReturn($this->resource); + + $this->resource->expects($this->exactly(2)) + ->method('getTable') + ->willReturnCallback(function ($table) use (&$callIndexTableName, $tableName, $tableNameProduct) { + if ($callIndexTableName === 0) { + $this->assertEquals($tableName, $table); + $callIndexTableName++; + return $tableName; + } + + $this->assertEquals($tableNameProduct, $table); + return $tableNameProduct; + }); + + $this->_connection->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->once()) + ->method('from') + ->with(['url_rewrite' => $tableName], $dataProvider['fields']) + ->willReturn($this->select); + + $this->select->expects($this->once()) + ->method('joinLeft') + ->with(['cpe' => $tableNameProduct], 'cpe.entity_id = url_rewrite.entity_id') + ->willReturn($this->select); + + $callIndexSelect = 0; + $storeId = $dataProvider['store_id']; + + $this->select->expects($this->exactly(3)) + ->method('where') + ->willReturnCallback(function ($condition, $value) use (&$callIndexSelect, $storeId) { + if ($callIndexSelect === 0) { + $this->assertEquals('request_path IN (?)', $condition); + $this->assertEquals([$storeId => "adobe.html"], $value); + } elseif ($callIndexSelect === 1) { + $this->assertEquals('store_id IN (?)', $condition); + $this->assertEquals($storeId, $value); + } else { + $this->assertEquals('cpe.sku not in (?)', $condition); + } + $callIndexSelect++; + return $this->select; + }); + + $this->_connection->expects($this->once()) + ->method('fetchAssoc') + ->with($this->select) + ->willReturn([$dataProvider['entity']]); + + if ($dataProvider['is_error_expected']) { + if ($dataProvider['entity']['entity_type'] === 'product') { + $expectedErrorMessage = sprintf( + $dataProvider['error_message_template'], + $dataProvider['request_path'], + $dataProvider['entity']['sku'], + ); + } else { + $expectedErrorMessage = sprintf( + $dataProvider['error_message_template'], + $dataProvider['request_path'], + $dataProvider['entity']['entity_type'], + $dataProvider['entity']['entity_id'] + ); + } + + $errorAggregator->expects($this->once()) + ->method('addError') + ->with( + ValidatorInterface::ERROR_DUPLICATE_URL_KEY, + ProcessingError::ERROR_LEVEL_CRITICAL, + $this->productPropertiesMap['rowNumbers'][$storeId][$dataProvider['request_path']], + 'url_key', + $expectedErrorMessage + ); + } + + $this->invokeMethod( + $importProduct, + 'checkUrlKeyDuplicates', + ); + } + + /** + * Data provider for checking duplicated entries. + * + * @return array[] + */ + public static function duplicatedUrlCheckDataProvider(): array + { + return [ + 'Record duplicated by category. Should Throw Validation Error' => [ + 'data' => [ + 'is_error_expected' => true, + 'store_id' => 0, + 'table_name' => 'url_rewrite', + 'table_name_product' => 'catalog_product_entity', + 'request_path' => 'adobe.html', + 'entity' => [ + 'request_path' => 'adobe.html', + 'store_id' => 0, + 'entity_type' => 'category', + 'entity_id' => rand(), + 'url_rewrite_id' => rand(), + 'url_entity' => rand() + ], + 'error_message_template' => 'Url key: \'%s\' was already generated for a %s with the ID: %s. ' . + 'You need to specify the unique URL key manually', + 'fields' => [ + 'request_path', + 'store_id', + 'entity_type' + ] + ] + ], + 'Record duplicated by product. Should Throw Validation Error' => [ + 'data' => [ + 'is_error_expected' => true, + 'store_id' => 0, + 'table_name' => 'url_rewrite', + 'table_name_product' => 'catalog_product_entity', + 'request_path' => 'adobe.html', + 'entity' => [ + 'request_path' => 'adobe.html', + 'store_id' => 0, + 'entity_type' => 'product', + 'entity_id' => 42, + 'url_rewrite_id' => rand(), + 'url_entity' => rand(), + 'sku' => rand() + ], + 'fields' => [ + 'request_path', + 'store_id', + 'entity_type', + ], + 'error_message_template' => 'Url key: \'%s\' was already generated for an item with the SKU: ' . + '\'%s\'. You need to specify the unique URL key manually' + ] + ] + ]; + } + + /** + * Set up properties map. + * + * @param array $dataProvider + * + * @return MockObject + */ + private function setUpPropertiesMap(array $dataProvider): MockObject + { + $errorAggregator = $this->getMockBuilder(ProcessingErrorAggregatorInterface::class) + ->getMock(); + + $this->resource = $this->getMockBuilder(AbstractEntity::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->productPropertiesMap['urlKeys'] = [[$dataProvider['request_path'] => 'Entity Name']]; + $this->productPropertiesMap['rowNumbers'] = [$dataProvider['store_id'] => [$dataProvider['request_path'] => 1]]; + $this->productPropertiesMap['errorAggregator'] = $errorAggregator; + + return $errorAggregator; + } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js index 6cc7849683a3..ead5e9fd32cf 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js @@ -1,6 +1,6 @@ /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. + * Copyright 2014 Adobe + * All Rights Reserved. */ /** @@ -84,6 +84,11 @@ define([ $.each(elements, function (index, field) { uiRegistry.async(formPath + '.' + field)(self.doElementBinding.bind(self)); }); + let regionId = uiRegistry.async(formPath + '.region_id'); + + if (regionId() !== undefined) { + this.bindHandler(regionId(), self.validateDelay); + } }, /** diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index fdcfb0c5705f..20b54b82a223 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -1,9 +1,8 @@ timezone = $timezone; + } + + /** + * Validate dob field. + * + * @param Customer $customer + * @return bool + */ + public function isValid($customer): bool + { + $storeId = (int)$customer->getStoreId(); + $timezone = new DateTimeZone($this->timezone->getConfigTimezone(ScopeInterface::SCOPE_STORE, $storeId)); + + if (!$this->isValidDob($customer->getDob(), $timezone)) { + $this->_addMessages([['dob' => 'The Date of Birth should not be greater than today.']]); + } + + return count($this->_messages) === 0; + } + + /** + * Check if specified dob is not in the future + * + * @param string|null $dobValue + * @param DateTimeZone $timezone + * @return bool + */ + private function isValidDob(?string $dobValue, DateTimeZone $timezone): bool + { + if ($dobValue) { + + // Get the date of birth and set the time to 00:00:00 + $dobDate = new \DateTime($dobValue, $timezone); + $dobDate->setTime(0, 0, 0); + + // Get the timestamp of the date of birth and the current date + $dobTimestamp = $dobDate->getTimestamp(); + $currentTimestamp = time(); + + // If the date's of birth first minute is in the future, return false - the day has not started yet + return ($dobTimestamp <= $currentTimestamp); + } + + return true; + } +} diff --git a/app/code/Magento/Customer/etc/validation.xml b/app/code/Magento/Customer/etc/validation.xml index 7fd6cfeb7947..5d6118170155 100644 --- a/app/code/Magento/Customer/etc/validation.xml +++ b/app/code/Magento/Customer/etc/validation.xml @@ -1,8 +1,8 @@ @@ -23,12 +23,18 @@ + + + + + + diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php index bf394c9ed0c4..11eaaa1a4a20 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php @@ -1,7 +1,7 @@ '', ] ); + $fieldset->addField( + '_import_history_id', + 'hidden', + [ + 'name' => '_import_history_id', + 'label' => __('Import History id'), + 'title' => __('Import History id'), + 'value' => '', + ] + ); $fieldsets['upload'] = $fieldset; $form->setUseContainer(true); $this->setForm($form); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php index 60169ddf7379..b974b483288d 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php @@ -1,6 +1,6 @@ getValidatedIds(); if (count($ids) > 0) { $resultBlock->addAction('value', Import::FIELD_IMPORT_IDS, $ids); + $resultBlock->addAction( + 'value', + '_import_history_id', + $this->historyModel->getId() + ); } } catch (\Magento\Framework\Exception\LocalizedException $e) { $resultBlock->addError($e->getMessage()); diff --git a/app/code/Magento/ImportExport/Model/History.php b/app/code/Magento/ImportExport/Model/History.php index 9a97367ba845..ad0be7fbb8bc 100644 --- a/app/code/Magento/ImportExport/Model/History.php +++ b/app/code/Magento/ImportExport/Model/History.php @@ -1,7 +1,7 @@ isReportEntityType()) { - $this->load($this->getLastItemId()); + $this->load($import->getData('_import_history_id') ?? $this->getLastItemId()); $executionResult = self::IMPORT_IN_PROCESS; if ($updateSummary) { $executionResult = $this->reportHelper->getExecutionTime($this->getStartedAt()); diff --git a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Import/ValidateTest.php b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Import/ValidateTest.php index 1e03e0ad3ca6..5b346720924d 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Import/ValidateTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Import/ValidateTest.php @@ -1,7 +1,7 @@ expects($this->once()) + ->method('addAction') + ->willReturn( + ['show', 'import_validation_container'], + ['value', '_import_history_id', 1] + ); $this->importMock->expects($this->exactly(3)) ->method('getProcessedRowsCount') ->willReturn(2); diff --git a/app/code/Magento/ImportExport/i18n/en_US.csv b/app/code/Magento/ImportExport/i18n/en_US.csv index cc1098841bab..378daac3afa2 100644 --- a/app/code/Magento/ImportExport/i18n/en_US.csv +++ b/app/code/Magento/ImportExport/i18n/en_US.csv @@ -29,6 +29,7 @@ Import,Import "File to Import","File to Import" "Select File to Import","Select File to Import" "Images File Directory","Images File Directory" +"Import History id","Import History id" "For Type ""Local Server"" use relative path to <Magento root directory>/var/import/images, e.g. product_images, import_images/batch1.

For example, in case product_images, files should be placed into <Magento root directory>/var/import/images/product_images folder.","For Type ""Local Server"" use relative path to <Magento root directory>/var/import/images, e.g. product_images, import_images/batch1.

For example, in case product_images, files should be placed into <Magento root directory>/var/import/images/product_images folder." "Download Sample File","Download Sample File" "Please correct the data sent value.","Please correct the data sent value." diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php index 9683c4f4524f..2e0a001c741b 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -124,7 +124,9 @@ private function findRelations( $relatedProducts = []; /** @var ProductInterface $item */ foreach ($relatedSearchResult->getItems() as $item) { - $relatedProducts[$item->getId()] = $item; + if ($item->isAvailable()) { + $relatedProducts[$item->getId()] = $item; + } } //Matching products with related products. diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index 2eb289e2e07d..887dac2a598c 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -1,9 +1,8 @@ _sitemapData = $sitemapData; $this->filesystem = $filesystem; $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::PUB); + $this->tmpDirectory = $filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); $this->_categoryFactory = $categoryFactory; $this->_productFactory = $productFactory; $this->_cmsFactory = $cmsFactory; @@ -482,11 +479,9 @@ public function generateXml() if ($this->_sitemapIncrement == 1) { // In case when only one increment file was created use it as default sitemap - $sitemapPath = $this->getSitemapPath() !== null ? rtrim($this->getSitemapPath(), '/') : ''; - $path = $sitemapPath . '/' . $this->_getCurrentSitemapFilename($this->_sitemapIncrement); - $destination = $sitemapPath . '/' . $this->getSitemapFilename(); - - $this->_directory->renameFile($path, $destination); + $path = $this->getFilePath($this->_getCurrentSitemapFilename($this->_sitemapIncrement)); + $destination = $this->getFilePath($this->getSitemapFilename()); + $this->tmpDirectory->renameFile($path, $destination, $this->_directory); } else { // Otherwise create index file with list of generated sitemaps $this->_createSitemapIndex(); @@ -507,10 +502,15 @@ protected function _createSitemapIndex() { $this->_createSitemap($this->getSitemapFilename(), self::TYPE_INDEX); for ($i = 1; $i <= $this->_sitemapIncrement; $i++) { - $xml = $this->_getSitemapIndexRow($this->_getCurrentSitemapFilename($i), $this->_getCurrentDateTime()); + $fileName = $this->_getCurrentSitemapFilename($i); + $path = $this->getFilePath($fileName); + $this->tmpDirectory->renameFile($path, $path, $this->_directory); + $xml = $this->_getSitemapIndexRow($fileName, $this->_getCurrentDateTime()); $this->_writeSitemapRow($xml); } $this->_finalizeSitemap(self::TYPE_INDEX); + $path = $this->getFilePath($this->getSitemapFilename()); + $this->tmpDirectory->renameFile($path, $path, $this->_directory); } /** @@ -638,9 +638,8 @@ protected function _createSitemap($fileName = null, $type = self::TYPE_URL) $this->_sitemapIncrement++; $fileName = $this->_getCurrentSitemapFilename($this->_sitemapIncrement); } - - $path = ($this->getSitemapPath() !== null ? rtrim($this->getSitemapPath(), '/') : '') . '/' . $fileName; - $this->_stream = $this->_directory->openFile($path); + $path = $this->getFilePath($fileName); + $this->_stream = $this->tmpDirectory->openFile($path); $fileHeader = sprintf($this->_tags[$type][self::OPEN_TAG_KEY], $type); $this->_stream->write($fileHeader); @@ -688,6 +687,20 @@ protected function _getCurrentSitemapFilename($index) . '-' . $this->getStoreId() . '-' . $index . '.xml'; } + /** + * Get path to sitemap file + * + * @param string $fileName + * @return string + */ + private function getFilePath(string $fileName): string + { + $path = $this->getSitemapPath() !== null ? rtrim($this->getSitemapPath(), '/') : ''; + $path .= '/' . $fileName; + + return $path; + } + /** * Get base dir * @@ -815,6 +828,7 @@ public function getSitemapUrl($sitemapPath, $sitemapFileName) * @return bool * @deprecated 100.1.5 Because the robots.txt file is not generated anymore, * this method is not needed and will be removed in major release. + * @see no alternatives */ protected function _isEnabledSubmissionRobots() { @@ -829,6 +843,7 @@ protected function _isEnabledSubmissionRobots() * @return void * @deprecated 100.1.5 Because the robots.txt file is not generated anymore, * this method is not needed and will be removed in major release. + * @see no alternatives */ protected function _addSitemapToRobotsTxt($sitemapFileName) { diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php index 0ccd32729c1a..6c40d257876d 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php @@ -1,12 +1,13 @@ sitemapCategoryMock = $this->getMockBuilder(Category::class) - ->disableOriginalConstructor() - ->getMock(); - $this->sitemapProductMock = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); - $this->sitemapCmsPageMock = $this->getMockBuilder(Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->helperMockSitemap = $this->getMockBuilder(Data::class) - ->disableOriginalConstructor() - ->getMock(); - $resourceMethods = [ '_construct', 'beginTransaction', @@ -133,35 +107,37 @@ protected function setUp(): void 'commit', '__wakeup', ]; - $this->resourceMock = $this->getMockBuilder(SitemapResource::class) ->onlyMethods($resourceMethods) ->disableOriginalConstructor() ->getMock(); - $this->resourceMock->method('addCommitCallback') ->willReturnSelf(); $this->fileMock = $this->createMock(Write::class); - $this->directoryMock = $this->createMock(DirectoryWrite::class); - $this->directoryMock->method('openFile') ->willReturn($this->fileMock); - - $this->filesystemMock = $this->getMockBuilder(Filesystem::class) - ->onlyMethods(['getDirectoryWrite']) - ->disableOriginalConstructor() - ->getMock(); - + $this->tmpFileMock = $this->createMock(Write::class); + $this->tmpDirectoryMock = $this->createMock(DirectoryWrite::class); + $this->tmpDirectoryMock->method('openFile') + ->willReturn($this->tmpFileMock); + $this->mediaDirectoryMock = $this->createMock(DirectoryWrite::class); + $this->filesystemMock = $this->createMock(Filesystem::class); $this->filesystemMock->method('getDirectoryWrite') - ->willReturn($this->directoryMock); + ->willReturnMap( + [ + [DirectoryList::PUB, $this->directoryMock], + [DirectoryList::SYS_TMP, $this->tmpDirectoryMock], + [DirectoryList::MEDIA, $this->mediaDirectoryMock], + ] + ); + + $this->configReaderMock = $this->createMock(SitemapConfigReaderInterface::class); + $this->itemProviderMock = $this->createMock(ItemProviderInterface::class); - $this->configReaderMock = $this->getMockForAbstractClass(SitemapConfigReaderInterface::class); - $this->itemProviderMock = $this->getMockForAbstractClass(ItemProviderInterface::class); - $this->request = $this->createMock(Http::class); $this->store = $this->createPartialMock(Store::class, ['isFrontUrlSecure', 'getBaseUrl']); - $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); $this->storeManagerMock->method('getStore') ->willReturn($this->store); } @@ -423,35 +399,28 @@ protected function prepareSitemapModelMock( } $actualData[$currentFile] .= $str; }; - // Check that all expected lines were written - $this->fileMock->expects( - $this->exactly($expectedWrites) - )->method( - 'write' - )->willReturnCallback( - $streamWriteCallback - ); + $this->tmpFileMock->expects($this->exactly($expectedWrites)) + ->method('write') + ->willReturnCallback($streamWriteCallback); $checkFileCallback = function ($file) use (&$currentFile) { $currentFile = $file; - };// Check that all expected file descriptors were created - $this->directoryMock->expects($this->exactly(count($expectedFile)))->method('openFile') + }; + // Check that all expected file descriptors were created + $this->tmpDirectoryMock->expects($this->exactly(count($expectedFile))) + ->method('openFile') ->willReturnCallback($checkFileCallback); // Check that all file descriptors were closed - $this->fileMock->expects($this->exactly(count($expectedFile))) + $this->tmpFileMock->expects($this->exactly(count($expectedFile))) ->method('close'); if (count($expectedFile) == 1) { - $this->directoryMock->expects($this->once()) + $this->tmpDirectoryMock->expects($this->once()) ->method('renameFile') - ->willReturnCallback( - function ($from, $to) { - Assert::assertEquals('/sitemap-1-1.xml', $from); - Assert::assertEquals('/sitemap.xml', $to); - } - ); + ->with('/sitemap-1-1.xml', '/sitemap.xml', $this->directoryMock) + ->willReturn(true); } // Check robots txt @@ -591,17 +560,11 @@ protected function getModelMock($mockBeforeSave = false) */ private function getModelConstructorArgs() { - $categoryFactory = $this->getMockBuilder(CategoryFactory::class) - ->disableOriginalConstructor() - ->getMock(); - - $productFactory = $this->getMockBuilder(ProductFactory::class) - ->disableOriginalConstructor() - ->getMock(); - - $cmsFactory = $this->getMockBuilder(PageFactory::class) - ->disableOriginalConstructor() - ->getMock(); + $categoryFactory = $this->createMock(CategoryFactory::class); + $productFactory = $this->createMock(ProductFactory::class); + $cmsFactory = $this->createMock(PageFactory::class); + $helperMockSitemap = $this->createMock(Data::class); + $request = $this->createMock(Http::class); $objectManager = new ObjectManager($this); $escaper = $objectManager->getObject(Escaper::class); @@ -614,12 +577,12 @@ private function getModelConstructorArgs() 'productFactory' => $productFactory, 'cmsFactory' => $cmsFactory, 'storeManager' => $this->storeManagerMock, - 'sitemapData' => $this->helperMockSitemap, + 'sitemapData' => $helperMockSitemap, 'filesystem' => $this->filesystemMock, 'itemProvider' => $this->itemProviderMock, 'configReader' => $this->configReaderMock, 'escaper' => $escaper, - 'request' => $this->request, + 'request' => $request, ] ); $constructArguments['resource'] = null; diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php index 034c9c10269e..f70ac358f7ce 100644 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -1,8 +1,9 @@ customerAddressFactory->create(); - $customerAddress->setCountryId($address->getCountryId()); - $customerAddress->setRegion( - $this->customerAddressRegionFactory->create()->setRegionId($address->getRegionId()) + $region = $this->customerAddressRegionFactory->create( + [ + 'data' => [ + 'region_id' => $address->getRegionId(), + 'region_code' => $address->getRegionCode(), + 'region' => $address->getRegion() + ] + ] ); - $customerAddress->setPostcode($address->getPostcode()); - $customerAddress->setCity($address->getCity()); - $customerAddress->setStreet($address->getStreet()); - return $customerAddress; + return $this->customerAddressFactory->create( + [ + 'data' => [ + 'country_id' => $address->getCountryId(), + 'region' => $region, + 'postcode' => $address->getPostcode(), + 'city' => $address->getCity(), + 'street' => $address->getStreet() + ] + ] + ); } /** diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php index 58c048fd771e..892e535377f5 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php @@ -1,13 +1,17 @@ taxConfig = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->onlyMethods(['getShippingTaxClass', 'shippingPriceIncludesTax', 'discountTax']) @@ -117,8 +154,13 @@ protected function setUp(): void ->method('getQuote') ->willReturn($this->quote); $methods = ['create']; - $this->quoteDetailsItemDataObject = $objectManager->getObject(ItemDetails::class); - $this->taxClassKeyDataObject = $objectManager->getObject(TaxClassKey::class); + $this->quoteDetailsItemDataObject = $this->createMock(ItemDetails::class); + $this->quoteDetailsItemDataObject->method('setType')->willReturnSelf(); + $this->quoteDetailsItemDataObject->method('setCode')->willReturnSelf(); + $this->quoteDetailsItemDataObject->method('setQuantity')->willReturnSelf(); + $this->taxClassKeyDataObject = $this->createMock(TaxClassKey::class); + $this->taxClassKeyDataObject->method('setType')->willReturnSelf(); + $this->taxClassKeyDataObject->method('setValue')->willReturnSelf(); $this->quoteDetailsItemDataObjectFactoryMock = $this->createPartialMock(QuoteDetailsItemInterfaceFactory::class, $methods); $this->quoteDetailsItemDataObjectFactoryMock @@ -132,15 +174,78 @@ protected function setUp(): void $this->taxHelper = $this->getMockBuilder(TaxHelper::class) ->disableOriginalConstructor() ->getMock(); - $this->commonTaxCollector = $objectManager->getObject( - CommonTaxCollector::class, - [ - 'taxConfig' => $this->taxConfig, - 'quoteDetailsItemDataObjectFactory' => $this->quoteDetailsItemDataObjectFactoryMock, - 'taxClassKeyDataObjectFactory' => $this->taxClassKeyDataObjectFactoryMock, - 'taxHelper' => $this->taxHelper, - ] + $this->taxCalculation = $this->createMock(TaxCalculationInterface::class); + $this->quoteDetailsFactory = $this->createMock(QuoteDetailsInterfaceFactory::class); + $this->addressFactory = $this->createMock(AddressInterfaceFactory::class); + $this->regionFactory = $this->createMock(RegionInterfaceFactory::class); + $this->quoteDetailsItemExtensionFactory = $this->createMock(QuoteDetailsItemExtensionInterfaceFactory::class); + $this->accountManagement = $this->createMock(AccountManagementInterface::class); + $this->commonTaxCollector = new CommonTaxCollector( + $this->taxConfig, + $this->taxCalculation, + $this->quoteDetailsFactory, + $this->quoteDetailsItemDataObjectFactoryMock, + $this->taxClassKeyDataObjectFactoryMock, + $this->addressFactory, + $this->regionFactory, + $this->taxHelper, + $this->quoteDetailsItemExtensionFactory, + $this->accountManagement ); + + parent::setUp(); + } + + /** + * @return void + * @throws Exception + */ + public function testMapAddress(): void + { + $countryId = 1; + $regionId = 2; + $regionCode = 'regionCode'; + $region = 'region'; + $postCode = 'postCode'; + $city = 'city'; + $street = ['street']; + + $address = $this->createMock(QuoteAddress::class); + $address->expects($this->once())->method('getCountryId')->willReturn($countryId); + $address->expects($this->once())->method('getRegionId')->willReturn($regionId); + $address->expects($this->once())->method('getRegionCode')->willReturn($regionCode); + $address->expects($this->once())->method('getRegion')->willReturn($region); + $address->expects($this->once())->method('getPostcode')->willReturn($postCode); + $address->expects($this->once())->method('getCity')->willReturn($city); + $address->expects($this->once())->method('getStreet')->willReturn($street); + + $regionData = [ + 'data' => [ + 'region_id' => $regionId, + 'region_code' => $regionCode, + 'region' => $region, + ] + ]; + $regionObject = $this->createMock(RegionInterface::class); + $this->regionFactory->expects($this->once())->method('create')->with($regionData)->willReturn($regionObject); + $customerAddress = $this->createMock(AddressInterface::class); + + $this->addressFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'data' => [ + 'country_id' => $countryId, + 'region' => $regionObject, + 'postcode' => $postCode, + 'city' => $city, + 'street' => $street + ] + ] + ) + ->willReturn($customerAddress); + + $this->assertSame($customerAddress, $this->commonTaxCollector->mapAddress($address)); } /** @@ -153,12 +258,13 @@ protected function setUp(): void * * @return void * @dataProvider getShippingDataObjectDataProvider + * @throws Exception */ public function testGetShippingDataObject( array $addressData, - $useBaseCurrency, - $shippingTaxClass, - $shippingPriceInclTax + bool $useBaseCurrency, + string $shippingTaxClass, + bool $shippingPriceInclTax ): void { $shippingAssignmentMock = $this->getMockForAbstractClass(ShippingAssignmentInterface::class); /** @var MockObject|QuoteAddressTotal $totalsMock */ @@ -201,10 +307,8 @@ public function testGetShippingDataObject( ->expects($this->once()) ->method('getBaseShippingDiscountAmount') ->willReturn($baseShippingAmount); - $expectedDiscountAmount = $baseShippingAmount; } else { $totalsMock->expects($this->never())->method('getBaseShippingDiscountAmount'); - $expectedDiscountAmount = $shippingAmount; } } foreach ($addressData as $key => $value) { @@ -214,10 +318,6 @@ public function testGetShippingDataObject( $this->quoteDetailsItemDataObject, $this->commonTaxCollector->getShippingDataObject($shippingAssignmentMock, $totalsMock, $useBaseCurrency) ); - - if ($shippingAmount) { - $this->assertEquals($expectedDiscountAmount, $this->quoteDetailsItemDataObject->getDiscountAmount()); - } } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php index b7f3e5687a11..65a718f67b2d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php @@ -1,7 +1,7 @@ [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'CreateAccount', + ], + ]; + + $customerDataArray = $this->dataObjectProcessor->buildOutputDataArray( + $this->customerHelper->createSampleCustomerDataObject(), + \Magento\Customer\Api\Data\CustomerInterface::class + ); + $date = new \DateTime(); + $date->modify('+1 month'); + $futureDob = $date->format('Y-m-d'); + $customerDataArray['dob'] = $futureDob; + $requestData = ['customer' => $customerDataArray, 'password' => CustomerHelper::PASSWORD]; + try { + $this->_webApiCall($serviceInfo, $requestData); + $this->fail('Expected exception did not occur.'); + } catch (\Exception $e) { + if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { + $expectedException = new InputException(); + $expectedException->addError(__('The Date of Birth should not be greater than today.')); + $this->assertInstanceOf('SoapFault', $e); + $this->checkSoapFault( + $e, + $expectedException->getRawMessage(), + 'env:Sender', + $expectedException->getParameters() // expected error parameters + ); + } else { + $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode()); + $exceptionData = $this->processRestExceptionResult($e); + $expectedExceptionData = [ + 'message' => 'The Date of Birth should not be greater than today.', + ]; + $this->assertEquals($expectedExceptionData, $exceptionData); + } + } + } public function testCreateCustomerWithoutOptionalFields() { $serviceInfo = [ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php index 2bcee4e16851..c6666b78ebb2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php @@ -1,7 +1,7 @@ name(), - $exception + $exception, + $test ); } @@ -125,9 +122,8 @@ protected function _applyFixtures(array $fixtures, TestCase $test) } catch (\Throwable $exception) { ExceptionHandler::handle( 'Unable to apply fixture: ' . $this->getFixtureReference($fixture), - $fixture['test']['class'], - $fixture['test']['method'], - $exception + $exception, + $test ); } $this->_appliedFixtures[] = $fixture; @@ -155,9 +151,8 @@ protected function _revertFixtures(?TestCase $test = null) } catch (\Throwable $exception) { ExceptionHandler::handle( 'Unable to revert fixture: ' . $this->getFixtureReference($fixture), - $fixture['test']['class'], - $fixture['test']['method'], - $exception + $exception, + $test ); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AppArea.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AppArea.php index 46927458ec73..015a596e3b7f 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AppArea.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AppArea.php @@ -1,7 +1,7 @@ name(), - $exception + $exception, + $test ); } if ($values) { diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/Cache.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/Cache.php index 6d027f10a742..29130f167ce2 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/Cache.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/Cache.php @@ -1,9 +1,8 @@ getName(false), - $exception + $exception, + $test ); } @@ -93,8 +91,7 @@ private function setValues($values, TestCase $test) if (!isset($this->origValues[$type])) { ExceptionHandler::handle( "Unknown cache type specified: '{$type}' in @magentoCache", - get_class($test), - $test->getName(false) + test: $test ); } $states->setEnabled($type, $isEnabled); diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/ComponentRegistrarFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/ComponentRegistrarFixture.php index 6288a8f1b5b6..79c18848cbdb 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/ComponentRegistrarFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/ComponentRegistrarFixture.php @@ -1,9 +1,8 @@ getName(false), - $exception + $exception, + $test ); } if (!$values) { diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DbIsolation.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DbIsolation.php index 3b1f41da9be5..19ff3912c485 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DbIsolation.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DbIsolation.php @@ -1,11 +1,10 @@ name(), - $exception + $exception, + $test ); } return $state; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/ExceptionHandler.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/ExceptionHandler.php index 31f20b93f225..2b4b03c1d5e2 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/ExceptionHandler.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/ExceptionHandler.php @@ -1,86 +1,40 @@ getMessage(), - (int) $e->getCode(), - $e - ); + ?\Throwable $previous = null, + ?TestCase $test = null + ): never { + if (!$test) { + throw new Exception($message, 0, $previous); } - $name = $testMethod; - - if ($name && $reflected->hasMethod($name)) { - try { - $reflected = $reflected->getMethod($name); - } catch (ReflectionException $e) { - throw new Exception( - $e->getMessage(), - (int) $e->getCode(), - $e - ); - } - } - - $location = sprintf( - "%s(%d): %s->%s()", - $reflected->getFileName(), - $reflected->getStartLine(), - $testClass, - $testMethod - ); - - $summary = ''; if ($previous) { - $exception = $previous; - do { - $summary .= PHP_EOL - . PHP_EOL - . 'Caused By: ' - . $exception->getMessage() - . PHP_EOL - . $exception->getTraceAsString(); - } while ($exception = $exception->getPrevious()); + $throwable = ThrowableBuilder::from($previous); + $message .= PHP_EOL . 'Caused by' . PHP_EOL . $throwable->asString(); } - throw new Exception( - sprintf( - "%s\n#0 %s%s", - $message, - $location, - $summary - ), - 0, - $previous - ); + $test::fail($message); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/IndexerDimensionMode.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/IndexerDimensionMode.php index 0bfccd989f2d..e054eba94383 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/IndexerDimensionMode.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/IndexerDimensionMode.php @@ -1,7 +1,7 @@ getName(false) + test: $test ); } } @@ -106,9 +105,8 @@ public function startTest(TestCase $test) } catch (\Throwable $exception) { ExceptionHandler::handle( 'Unable to parse fixtures', - get_class($test), - $test->getName(false), - $exception + $exception, + $test ); } @@ -117,8 +115,7 @@ public function startTest(TestCase $test) if ($dbIsolation) { ExceptionHandler::handle( '@magentoDbIsolation must be disabled when using @magentoIndexerDimensionMode', - get_class($test), - $test->getName(false) + test: $test ); } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Event/ExecutionState.php b/dev/tests/integration/framework/Magento/TestFramework/Event/ExecutionState.php new file mode 100644 index 000000000000..d4fc43e51866 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Event/ExecutionState.php @@ -0,0 +1,55 @@ +data[$test]['exception'] = $exception; + } + + /** + * Pop failure registered during preparation phase. + * + * @param string $test + * @return \Throwable|null + */ + public function popPreparationFailure(string $test): ?\Throwable + { + $exception = $this->data[$test]['exception'] ?? null; + if ($exception) { + unset($this->data[$test]['exception']); + } + + return $exception; + } + + /** + * Clear stored test data. + * + * @param string $test + * @return void + */ + public function clearTestData(string $test): void + { + unset($this->data[$test]); + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Event/Subscribers.php b/dev/tests/integration/framework/Magento/TestFramework/Event/Subscribers.php index 7d9277f20e85..49ddacb65733 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Event/Subscribers.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Event/Subscribers.php @@ -3,15 +3,16 @@ * Copyright 2024 Adobe * All Rights Reserved. */ +declare(strict_types=1); -/** - * Subscribers of PHPUnit built-in events - */ namespace Magento\TestFramework\Event; use PHPUnit\Runner; use PHPUnit\TextUI; +/** + * Subscribers of PHPUnit built-in events + */ class Subscribers implements Runner\Extension\Extension { /** @@ -20,25 +21,27 @@ class Subscribers implements Runner\Extension\Extension * @param TextUI\Configuration\Configuration $configuration * @param Runner\Extension\Facade $facade * @param Runner\Extension\ParameterCollection $parameters + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function bootstrap( TextUI\Configuration\Configuration $configuration, Runner\Extension\Facade $facade, Runner\Extension\ParameterCollection $parameters ): void { + $executionState = new ExecutionState(); if ($configuration->hasConfigurationFile() && str_contains($configuration->configurationFile(), 'setup-integration')) { $facade->registerSubscribers( - new TestPreprationStartedSubscriber(), - new TestFinishedSubscriber() + new TestPreprationStartedSubscriber($executionState), + new TestFinishedSubscriber($executionState) ); } else { $facade->registerSubscribers( new TestSuitStartedSubscriber(), new TestSuitEndSubscriber(), - new TestPreparedSubscriber(), - new TestPreprationStartedSubscriber(), - new TestFinishedSubscriber(), + new TestPreparedSubscriber($executionState), + new TestPreprationStartedSubscriber($executionState), + new TestFinishedSubscriber($executionState), new TestSkippedSubscriber(), new TestErroredSubscriber() ); diff --git a/dev/tests/integration/framework/Magento/TestFramework/Event/TestFinishedSubscriber.php b/dev/tests/integration/framework/Magento/TestFramework/Event/TestFinishedSubscriber.php index 73d8cddc4ce6..f03c9fe2cc44 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Event/TestFinishedSubscriber.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Event/TestFinishedSubscriber.php @@ -3,18 +3,24 @@ * Copyright 2024 Adobe * All Rights Reserved. */ +declare(strict_types=1); -/** - * Test Finished Subscriber - */ namespace Magento\TestFramework\Event; use PHPUnit\Event\Test\FinishedSubscriber; use PHPUnit\Event\Test\Finished; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; class TestFinishedSubscriber implements FinishedSubscriber { + /** + * @param ExecutionState $executionState + */ + public function __construct(private readonly ExecutionState $executionState) + { + } + /** * Test finished Subscriber * @@ -24,12 +30,14 @@ public function notify(Finished $event): void { $className = $event->test()->className(); $methodName = $event->test()->methodName(); - $objectManager = Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); + /** @var TestCase $testObj */ $testObj = $objectManager->create($className, ['name' => $methodName]); $phpUnit = $objectManager->create(PhpUnit::class); $phpUnit->endTest($testObj, 0); + $this->executionState->clearTestData($testObj->toString()); Magento::setCurrentEventObject(null); Magento::setTestPrepared(false); } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Event/TestPreparedSubscriber.php b/dev/tests/integration/framework/Magento/TestFramework/Event/TestPreparedSubscriber.php index 8a03af2d84ad..f0f17871f853 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Event/TestPreparedSubscriber.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Event/TestPreparedSubscriber.php @@ -3,10 +3,8 @@ * Copyright 2024 Adobe * All Rights Reserved. */ +declare(strict_types=1); -/** - * Test Prepared Subscriber - */ namespace Magento\TestFramework\Event; use PHPUnit\Event\Test\Prepared; @@ -16,6 +14,13 @@ class TestPreparedSubscriber implements PreparedSubscriber { + /** + * @param ExecutionState $executionState + */ + public function __construct(private readonly ExecutionState $executionState) + { + } + /** * Test prepared Subscriber * @@ -29,6 +34,13 @@ public function notify(Prepared $event): void $objectManager = Bootstrap::getObjectManager(); $testObj = $objectManager->create($className, ['name' => $methodName]); + // An exception can occur in PreparationStarted subscriber during applying fixtures. + // In order to prevent test execution it should be thrown here, from Prepared subscriber. + $exception = $this->executionState->popPreparationFailure($testObj->toString()); + if ($exception) { + throw $exception; + } + $testData = $event->test()->testData(); if ($testData->hasDataFromDataProvider()) { $dataSetName = $testData->dataFromDataProvider()->dataSetName(); diff --git a/dev/tests/integration/framework/Magento/TestFramework/Event/TestPreprationStartedSubscriber.php b/dev/tests/integration/framework/Magento/TestFramework/Event/TestPreprationStartedSubscriber.php index 7e8dfcd7addc..8ec5ae777008 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Event/TestPreprationStartedSubscriber.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Event/TestPreprationStartedSubscriber.php @@ -3,18 +3,26 @@ * Copyright 2024 Adobe * All Rights Reserved. */ +declare(strict_types=1); -/** - * TestPreparation Started Subscriber - */ namespace Magento\TestFramework\Event; use PHPUnit\Event\Test\PreparationStarted; use PHPUnit\Event\Test\PreparationStartedSubscriber; use Magento\TestFramework\Helper\Bootstrap; +/** + * TestPreparation Started Subscriber + */ class TestPreprationStartedSubscriber implements PreparationStartedSubscriber { + /** + * @param ExecutionState $executionState + */ + public function __construct(private readonly ExecutionState $executionState) + { + } + /** * Test Preparation Started Subscriber * @@ -31,6 +39,10 @@ public function notify(PreparationStarted $event): void Magento::setCurrentEventObject($event); $phpUnit = $objectManager->create(PhpUnit::class); - $phpUnit->startTest($testObj); + try { + $phpUnit->startTest($testObj); + } catch (\Throwable $e) { + $this->executionState->registerPreparationFailure($testObj->toString(), $e); + } } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox/RenderingBasedOnIsProductListFlagTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox/RenderingBasedOnIsProductListFlagTest.php index 5035bb34a8f9..a9e7a64e5af4 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox/RenderingBasedOnIsProductListFlagTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox/RenderingBasedOnIsProductListFlagTest.php @@ -1,17 +1,22 @@ get(ProductAttributeRepositoryInterface::class); + $specialPrice = $productAttributeRepository->get('special_price'); + $specialPrice->setUsedInProductListing(false); + $productAttributeRepository->save($specialPrice); + + try { + self::assertTrue($this->finalPriceBox->hasSpecialPrice()); + } finally { + $specialPrice->setUsedInProductListing(true); + $productAttributeRepository->save($specialPrice); + } + } + /** * Test when is_product_list flag is specified *