diff --git a/composer.json b/composer.json index af828662..f9f9caae 100644 --- a/composer.json +++ b/composer.json @@ -2,37 +2,37 @@ "name": "magento/magento-cloud-patches", "description": "Provides critical fixes for Magento 2 Enterprise Edition", "type": "magento2-component", - "version": "1.0.25", + "version": "1.1.10", "license": "OSL-3.0", "repositories": { "repo.magento.com": { - "type": "composer", - "url": "https://repo.magento.com/" + "type": "composer", + "url": "https://repo.magento.com/" } }, "require": { - "php": "^7.2 || ^8.0", + "php": "^8.0", "ext-json": "*", - "composer/composer": "^1.4 || ^2.0", + "composer/composer": "^1.9 || ^2.8 || !=2.2.16", "composer/semver": "@stable", - "symfony/config": "^3.3||^4.4||^5.0||^6.0", - "symfony/console": "^2.6||^4.0||^5.0||^6.0", - "symfony/dependency-injection": "^3.3||^4.3||^5.0||^6.0", - "symfony/process": "^2.1||^4.1||^5.0||^6.0", + "monolog/monolog": "^2.3 || ^2.7 || ^3.6", + "symfony/config": "^4.4 || ^5.1 || ^5.4 || ^6.4 || ^7.2", + "symfony/console": "^4.4 || ^5.1 || ^5.4 || ^6.4 || ^7.2", + "symfony/dependency-injection": "^4.4 || ^5.1 || ^5.4 || ^6.4 || ^7.2", + "symfony/process": "^4.4 || ^5.1 || ^5.4 || ^6.4 || ^7.2", "symfony/proxy-manager-bridge": "^3.3||^4.3||^5.0||^6.0", - "symfony/yaml": "^3.3||^4.0||^5.0||^6.0", - "monolog/monolog": "^1.25||^2.3||^2.7", + "symfony/yaml": "^4.4 || ^5.1 || ^5.4 || ^6.4 || ^7.2", "magento/quality-patches": "^1.1.0" }, "require-dev": { - "codeception/codeception": "^4.1", - "codeception/module-asserts": "^1.2", - "codeception/module-db": "^1.0", - "codeception/module-phpbrowser": "^1.0", - "codeception/module-rest": "^1.2", - "consolidation/robo": "^1.2 || ^2.0", + "codeception/codeception": "^4.1 || ^5.1", + "codeception/module-asserts": "^1.2 || ^3.0", + "codeception/module-db": "^1.0 || ^3.0", + "codeception/module-phpbrowser": "^1.0 || ^3.0", + "codeception/module-rest": "^1.2 || ^3.0", + "consolidation/robo": "^1.2 || ^3.0 || ^4.0 || ^5.0", "phpmd/phpmd": "@stable", - "phpunit/phpunit": "^8.5 || ^9.5", + "phpunit/phpunit": "^10.0", "squizlabs/php_codesniffer": "^3.0" }, "bin": [ diff --git a/patches.json b/patches.json index ea81ffbb..c321c13e 100644 --- a/patches.json +++ b/patches.json @@ -280,6 +280,33 @@ }, "Enhanced Layout Cache Efficiency (memory usage reduced)": { ">=2.4.4 <2.4.7": "MCLOUD-11514__enhanced_layout_cache_efficiency__2.4.6-p3.patch" + }, + "Patch for CVE-2024-34102 - CosmicSting": { + ">=2.4.4 <2.4.4-p8": "MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.4.patch", + ">=2.4.5 <2.4.5-p7": "MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.5.patch", + ">=2.4.6 <2.4.6-p5": "MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.6.patch", + "2.4.7": "MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.7.patch" + }, + "Patch for CVE-2024-34102 - KeyRotation": { + ">=2.4.4 <2.4.4-p10": "MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.4.patch", + ">=2.4.5 <2.4.5-p9": "MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.5.patch", + ">=2.4.6 <2.4.6-p7": "MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.6.patch", + ">=2.4.7 <2.4.7-p2": "MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.7.patch" + }, + "Patch for CVE-2025-24434 - Improve-web-api-async": { + ">=2.4.4 <2.4.4-p12": "MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.4.patch", + ">=2.4.5 <2.4.5-p11": "MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.5.patch", + ">=2.4.6 <2.4.6-p9": "MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.6.patch", + ">=2.4.7 <2.4.7-p4": "MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.7.patch" + }, + "Patch - Improve-web-api-async-performance": { + ">=2.4.4 <2.4.4-p13 || >=2.4.5 <2.4.5-p12 || >=2.4.6 <2.4.6-p10 || >=2.4.7 <2.4.7-p5 || 2.4.8": "MCLOUD-13619__Improve_web_api_async_performance__2.4.x.patch" + }, + "Patch for CVE-2025-47109 - Improve-category-view": { + "2.4.8": "MCLOUD-13752__Patch_for_CVE-2025-47109_Improve_category_view__2.4.8.patch" + }, + "Patch for CVE-2025-47110 - Improve-admin-cache-efficiency": { + ">=2.4.4 <2.4.4-p14 || >=2.4.5 <2.4.5-p13 || >=2.4.6 <2.4.6-p11 || >=2.4.7 <2.4.7-p6 || 2.4.8": "MCLOUD-13753__Patch_for_CVE-2025-47110_improve-admin-cache-efficiency__2.4.x.patch" } }, "magento/module-paypal": { @@ -370,6 +397,15 @@ "magento/magento2-b2b-base": { "Layered navigation filter is present only when product is present on the listing page with enabled Shared catalog": { ">=1.1.5 <1.3.1": "MCLOUD-6923__layered_navigation_filter_is_present_only_when_product_is_present_on_the_listing_page_with_enabled_shared_catalog__2.3.5.patch" + }, + "Fields hydration on company account create request": { + ">=1.3.3 <1.3.3-p11 || >=1.3.4 <1.3.4-p10 || >=1.3.5 <1.3.5-p8 || >=1.4.2 <1.4.2-p3": "B2B-4051__fields_hydration_company_account_create_request__1.3.3.patch" + }, + "Fixes the issue where the file generated after Requisition List export is not removed from the var/ directory": { + ">=1.3.1 <1.3.6": "MCLOUD-11623__requisition_list_exports_saved_to_var_directory__2.4.5-p1.patch" + }, + "Fixes the issue where an SQL syntax error occurs due to the non-existence of the REGEXP_LIKE function when attempting to update the company_structure table.": { + "1.5.2": "MCLOUD-13605__B2B_SQL_syntax_error_due_to_the_REGEXP_LIKE_function__1.5.2.patch" } }, "magento/magento2-ee-base": { @@ -410,5 +446,10 @@ "Fix regexp cache tag validation": { ">=103.0.6 <103.0.7": "MCLOUD-10226__fix_regexp_cache_tag_validation__2.4.6.patch" } + }, + "magento/module-catalog-graph-ql": { + "AttributeReader should use Factory for Collection": { + ">=100.4.7 <100.4.8": "ACPT-1876__attribute_reader_should_use_factory_for_collection__2.4.7.patch" + } } } diff --git a/patches/ACPT-1876__attribute_reader_should_use_factory_for_collection__2.4.7.patch b/patches/ACPT-1876__attribute_reader_should_use_factory_for_collection__2.4.7.patch new file mode 100644 index 00000000..fae4769f --- /dev/null +++ b/patches/ACPT-1876__attribute_reader_should_use_factory_for_collection__2.4.7.patch @@ -0,0 +1,75 @@ +From c964bc3248811dc63df6205a1246d383ad4c6e4a Mon Sep 17 00:00:00 2001 +From: Jacob Brown +Date: Wed, 3 Apr 2024 14:07:21 -0500 +Subject: [PATCH] ACPT-1854: AttributeReader should use Factory for Collection + +--- + .../Model/Config/AttributeReader.php | 18 +++++++++++------- + 1 file changed, 11 insertions(+), 7 deletions(-) + +diff --git a/vendor/magento/module-catalog-graph-ql/Model/Config/AttributeReader.php b/vendor/magento/module-catalog-graph-ql/Model/Config/AttributeReader.php +index ecd83bf61ef0..05acb97e4bd7 100644 +--- a/vendor/magento/module-catalog-graph-ql/Model/Config/AttributeReader.php ++++ b/vendor/magento/module-catalog-graph-ql/Model/Config/AttributeReader.php +@@ -9,8 +9,10 @@ + use Magento\Catalog\Model\Product; + use Magento\Catalog\Model\ResourceModel\Eav\Attribute; + use Magento\CatalogGraphQl\Model\Resolver\Products\Attributes\Collection; ++use Magento\CatalogGraphQl\Model\Resolver\Products\Attributes\CollectionFactory; + use Magento\EavGraphQl\Model\Resolver\Query\Type; + use Magento\Framework\App\Config\ScopeConfigInterface; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\Config\ReaderInterface; + use Magento\Framework\GraphQl\Exception\GraphQlInputException; + use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface; +@@ -36,9 +38,9 @@ class AttributeReader implements ReaderInterface + private Type $typeLocator; + + /** +- * @var Collection ++ * @var CollectionFactory + */ +- private Collection $collection; ++ private CollectionFactory $collectionFactory; + + /** + * @var ScopeConfigInterface +@@ -48,18 +50,21 @@ class AttributeReader implements ReaderInterface + /** + * @param MapperInterface $mapper + * @param Type $typeLocator +- * @param Collection $collection ++ * @param Collection $collection @deprecated @see $collectionFactory + * @param ScopeConfigInterface $config ++ * @param CollectionFactory|null $collectionFactory ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __construct( + MapperInterface $mapper, + Type $typeLocator, + Collection $collection, +- ScopeConfigInterface $config ++ ScopeConfigInterface $config, ++ CollectionFactory $collectionFactory = null, + ) { + $this->mapper = $mapper; + $this->typeLocator = $typeLocator; +- $this->collection = $collection; ++ $this->collectionFactory = $collectionFactory ?? ObjectManager::getInstance()->get(CollectionFactory::class); + $this->config = $config; + } + +@@ -74,12 +79,11 @@ public function __construct( + public function read($scope = null) : array + { + $config = []; +- + if ($this->config->isSetFlag(self::XML_PATH_INCLUDE_DYNAMIC_ATTRIBUTES, ScopeInterface::SCOPE_STORE)) { + $typeNames = $this->mapper->getMappedTypes(Product::ENTITY); + + /** @var Attribute $attribute */ +- foreach ($this->collection->getAttributes() as $attribute) { ++ foreach ($this->collectionFactory->create()->getAttributes() as $attribute) { + $attributeCode = $attribute->getAttributeCode(); + $locatedType = $this->typeLocator->getType($attributeCode, Product::ENTITY) ?: 'String'; + $locatedType = TypeProcessor::NORMALIZED_ANY_TYPE === $locatedType ? 'String' : ucfirst($locatedType); diff --git a/patches/B2B-4051__fields_hydration_company_account_create_request__1.3.3.patch b/patches/B2B-4051__fields_hydration_company_account_create_request__1.3.3.patch new file mode 100644 index 00000000..c17eab6c --- /dev/null +++ b/patches/B2B-4051__fields_hydration_company_account_create_request__1.3.3.patch @@ -0,0 +1,144 @@ +new file mode 100644 +--- /dev/null ++++ b/vendor/magento/module-company/Model/Customer/AccountManagement/CompanyRequestHydrator.php +@@ -0,0 +1,66 @@ ++request = $request; ++ } ++ ++ /** ++ * Get and hydrate company data from HTTP request. ++ * ++ * @return array ++ */ ++ public function getCompanyDataFromRequest(): array ++ { ++ $result = []; ++ $companyData = $this->request->getPost('company', []); ++ foreach ($this->fieldsToSave as $item) { ++ if (isset($companyData[$item])) { ++ $result[$item] = $companyData[$item]; ++ } ++ } ++ ++ return $result; ++ } ++} +--- a/vendor/magento/module-company/Plugin/Customer/Api/AccountManagement.php ++++ b/vendor/magento/module-company/Plugin/Customer/Api/AccountManagement.php +@@ -11,17 +11,13 @@ + use Magento\Customer\Api\CustomerRepositoryInterface; + use Magento\Company\Api\CompanyManagementInterface; + use Magento\Framework\Exception\NoSuchEntityException; ++use Magento\Company\Model\Customer\AccountManagement\CompanyRequestHydrator; + + /** + * Plugin for AccountManagement. Processing company data. + */ + class AccountManagement + { +- /** +- * @var \Magento\Framework\App\Request\Http +- */ +- private $request; +- + /** + * @var \Magento\Company\Model\Email\Sender + */ +@@ -47,30 +43,35 @@ + */ + private $customerRepository; + ++ /** ++ * @var CompanyRequestHydrator ++ */ ++ private $companyRequestHydrator; ++ + /** + * AccountManagement constructor + * +- * @param \Magento\Framework\App\Request\Http $request + * @param \Magento\Company\Model\Email\Sender $companyEmailSender + * @param \Magento\Backend\Model\UrlInterface $urlBuilder + * @param \Magento\Company\Model\Customer\Company $customerCompany + * @param CompanyManagementInterface $companyManagement + * @param CustomerRepositoryInterface $customerRepository ++ * @param CompanyRequestHydrator $companyRequestHydrator + */ + public function __construct( +- \Magento\Framework\App\Request\Http $request, + \Magento\Company\Model\Email\Sender $companyEmailSender, + \Magento\Backend\Model\UrlInterface $urlBuilder, + \Magento\Company\Model\Customer\Company $customerCompany, + CompanyManagementInterface $companyManagement, +- CustomerRepositoryInterface $customerRepository ++ CustomerRepositoryInterface $customerRepository, ++ CompanyRequestHydrator $companyRequestHydrator + ) { +- $this->request = $request; + $this->companyEmailSender = $companyEmailSender; + $this->urlBuilder = $urlBuilder; + $this->customerCompany = $customerCompany; + $this->companyManagement = $companyManagement; + $this->customerRepository = $customerRepository; ++ $this->companyRequestHydrator = $companyRequestHydrator; + } + + /** +@@ -127,11 +128,7 @@ + \Magento\Customer\Api\AccountManagementInterface $subject, + \Magento\Customer\Api\Data\CustomerInterface $result + ) { +- $companyData = $this->request->getPost('company', []); +- if (isset($companyData['status'])) { +- unset($companyData['status']); +- } +- ++ $companyData = $this->companyRequestHydrator->getCompanyDataFromRequest(); + if (is_array($companyData) && !empty($companyData)) { + $jobTitle = $companyData['job_title'] ?? null; + $companyDataObject = $this->customerCompany->createCompany($result, $companyData, $jobTitle); diff --git a/patches/MCLOUD-11623__requisition_list_exports_saved_to_var_directory__2.4.5-p1.patch b/patches/MCLOUD-11623__requisition_list_exports_saved_to_var_directory__2.4.5-p1.patch new file mode 100644 index 00000000..bcd29915 --- /dev/null +++ b/patches/MCLOUD-11623__requisition_list_exports_saved_to_var_directory__2.4.5-p1.patch @@ -0,0 +1,22 @@ +diff --git a/vendor/magento/module-requisition-list/Controller/Requisition/Export.php b/vendor/magento/module-requisition-list/Controller/Requisition/Export.php +index e8332a7f1091..7eee2f51b7f4 100644 +--- a/vendor/magento/module-requisition-list/Controller/Requisition/Export.php ++++ b/vendor/magento/module-requisition-list/Controller/Requisition/Export.php +@@ -101,9 +101,15 @@ public function execute() + + $fileName = "{$requisitionList->getName()}.{$writer->getFileExtension()}"; + ++ $content = [ ++ 'type' => "string", ++ 'value' => $this->requisitionListExport->export(), ++ 'rm' => true, ++ ]; ++ + return $this->fileFactory->create( + $fileName, +- $this->requisitionListExport->export(), ++ $content, + DirectoryList::VAR_DIR, + $writer->getContentType() + ); + diff --git a/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.4.patch b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.4.patch new file mode 100644 index 00000000..5175edb0 --- /dev/null +++ b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.4.patch @@ -0,0 +1,62 @@ +diff --git a/vendor/magento/theme-frontend-blank/i18n/en_US.csv b/vendor/magento/theme-frontend-blank/i18n/en_US.csv +index a491a567a37..5e8bef787d2 100644 +--- a/vendor/magento/theme-frontend-blank/i18n/en_US.csv ++++ b/vendor/magento/theme-frontend-blank/i18n/en_US.csv +@@ -4,3 +4,4 @@ Summary,Summary + Menu,Menu + Account,Account + Settings,Settings ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/theme-frontend-luma/i18n/en_US.csv b/vendor/magento/theme-frontend-luma/i18n/en_US.csv +index 7bf9e0afaf0..00493cc05ba 100644 +--- a/vendor/magento/theme-frontend-luma/i18n/en_US.csv ++++ b/vendor/magento/theme-frontend-luma/i18n/en_US.csv +@@ -54,3 +54,4 @@ Footer,Footer + "Update to your %store_name shipment","Update to your %store_name shipment" + "Address Book","Address Book" + "Account Information","Account Information" ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/framework/Webapi/ServiceInputProcessor.php b/vendor/magento/framework/Webapi/ServiceInputProcessor.php +index 908a4e70140..cc019845b58 100644 +--- a/vendor/magento/framework/Webapi/ServiceInputProcessor.php ++++ b/vendor/magento/framework/Webapi/ServiceInputProcessor.php +@@ -153,6 +153,7 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface + * @return \Magento\Framework\Reflection\NameFinder + * + * @deprecated 100.1.0 ++ * @see nothing + */ + private function getNameFinder() + { +@@ -261,6 +262,7 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface + * @throws \Exception + * @throws SerializationException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) ++ * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function _createFromArray($className, $data) + { +@@ -268,6 +270,12 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface + // convert to string directly to avoid situations when $className is object + // which implements __toString method like \ReflectionObject + $className = (string) $className; ++ if (is_subclass_of($className, \SimpleXMLElement::class) ++ || is_subclass_of($className, \DOMElement::class)) { ++ throw new SerializationException( ++ new Phrase('Invalid data type') ++ ); ++ } + $class = new ClassReflection($className); + if (is_subclass_of($className, self::EXTENSION_ATTRIBUTES_TYPE)) { + $className = substr($className, 0, -strlen('Interface')); +diff --git a/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php b/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php +--- a/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php (revision 022e64b08a88658667bc2d6b922eada2b7910965) ++++ b/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php (revision 8d2b0c1c6b421cdcd7f62a48a5edc9b0211d92a2) +@@ -35,6 +35,7 @@ + public function __construct(DeploymentConfig $deploymentConfig, JwkFactory $jwkFactory) + { + $this->keys = preg_split('/\s+/s', trim((string)$deploymentConfig->get('crypt/key'))); ++ $this->keys = [end($this->keys)]; + //Making sure keys are large enough. + foreach ($this->keys as &$key) { + $key = str_pad($key, 2048, '&', STR_PAD_BOTH); diff --git a/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.5.patch b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.5.patch new file mode 100644 index 00000000..e5740c26 --- /dev/null +++ b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.5.patch @@ -0,0 +1,62 @@ +diff --git a/vendor/magento/theme-frontend-blank/i18n/en_US.csv b/vendor/magento/theme-frontend-blank/i18n/en_US.csv +index a491a567a37..5e8bef787d2 100644 +--- a/vendor/magento/theme-frontend-blank/i18n/en_US.csv ++++ b/vendor/magento/theme-frontend-blank/i18n/en_US.csv +@@ -4,3 +4,4 @@ Summary,Summary + Menu,Menu + Account,Account + Settings,Settings ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/theme-frontend-luma/i18n/en_US.csv b/vendor/magento/theme-frontend-luma/i18n/en_US.csv +index 7bf9e0afaf0..00493cc05ba 100644 +--- a/vendor/magento/theme-frontend-luma/i18n/en_US.csv ++++ b/vendor/magento/theme-frontend-luma/i18n/en_US.csv +@@ -54,3 +54,4 @@ Footer,Footer + "Update to your %store_name shipment","Update to your %store_name shipment" + "Address Book","Address Book" + "Account Information","Account Information" ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/framework/Webapi/ServiceInputProcessor.php b/vendor/magento/framework/Webapi/ServiceInputProcessor.php +index a5e881f4be5..a60f1dd7ba1 100644 +--- a/vendor/magento/framework/Webapi/ServiceInputProcessor.php ++++ b/vendor/magento/framework/Webapi/ServiceInputProcessor.php +@@ -153,6 +153,7 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface + * @return \Magento\Framework\Reflection\NameFinder + * + * @deprecated 100.1.0 ++ * @see nothing + */ + private function getNameFinder() + { +@@ -261,6 +262,7 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface + * @throws \Exception + * @throws SerializationException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) ++ * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function _createFromArray($className, $data) + { +@@ -268,6 +270,12 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface + // convert to string directly to avoid situations when $className is object + // which implements __toString method like \ReflectionObject + $className = (string) $className; ++ if (is_subclass_of($className, \SimpleXMLElement::class) ++ || is_subclass_of($className, \DOMElement::class)) { ++ throw new SerializationException( ++ new Phrase('Invalid data type') ++ ); ++ } + $class = new ClassReflection($className); + if (is_subclass_of($className, self::EXTENSION_ATTRIBUTES_TYPE)) { + $className = substr($className, 0, -strlen('Interface')); +diff --git a/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php b/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php +--- a/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php (revision 022e64b08a88658667bc2d6b922eada2b7910965) ++++ b/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php (revision 8d2b0c1c6b421cdcd7f62a48a5edc9b0211d92a2) +@@ -35,6 +35,7 @@ + public function __construct(DeploymentConfig $deploymentConfig, JwkFactory $jwkFactory) + { + $this->keys = preg_split('/\s+/s', trim((string)$deploymentConfig->get('crypt/key'))); ++ $this->keys = [end($this->keys)]; + //Making sure keys are large enough. + foreach ($this->keys as &$key) { + $key = str_pad($key, 2048, '&', STR_PAD_BOTH); diff --git a/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.6.patch b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.6.patch new file mode 100644 index 00000000..1296cc09 --- /dev/null +++ b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.6.patch @@ -0,0 +1,46 @@ +diff --git a/vendor/magento/theme-frontend-blank/i18n/en_US.csv b/vendor/magento/theme-frontend-blank/i18n/en_US.csv +index a491a567a37..5e8bef787d2 100644 +--- a/vendor/magento/theme-frontend-blank/i18n/en_US.csv ++++ b/vendor/magento/theme-frontend-blank/i18n/en_US.csv +@@ -4,3 +4,4 @@ Summary,Summary + Menu,Menu + Account,Account + Settings,Settings ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/theme-frontend-luma/i18n/en_US.csv b/vendor/magento/theme-frontend-luma/i18n/en_US.csv +index 7bf9e0afaf0..00493cc05ba 100644 +--- a/vendor/magento/theme-frontend-luma/i18n/en_US.csv ++++ b/vendor/magento/theme-frontend-luma/i18n/en_US.csv +@@ -54,3 +54,4 @@ Footer,Footer + "Update to your %store_name shipment","Update to your %store_name shipment" + "Address Book","Address Book" + "Account Information","Account Information" ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/framework/Webapi/ServiceInputProcessor.php b/vendor/magento/framework/Webapi/ServiceInputProcessor.php +index 9d7fd443508..65987772c23 100644 +--- a/vendor/magento/framework/Webapi/ServiceInputProcessor.php ++++ b/vendor/magento/framework/Webapi/ServiceInputProcessor.php +@@ -275,6 +275,12 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface + // convert to string directly to avoid situations when $className is object + // which implements __toString method like \ReflectionObject + $className = (string) $className; ++ if (is_subclass_of($className, \SimpleXMLElement::class) ++ || is_subclass_of($className, \DOMElement::class)) { ++ throw new SerializationException( ++ new Phrase('Invalid data type') ++ ); ++ } + $class = new ClassReflection($className); + if (is_subclass_of($className, self::EXTENSION_ATTRIBUTES_TYPE)) { + $className = substr($className, 0, -strlen('Interface')); +diff --git a/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php b/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php +--- a/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php (revision 022e64b08a88658667bc2d6b922eada2b7910965) ++++ b/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php (revision 8d2b0c1c6b421cdcd7f62a48a5edc9b0211d92a2) +@@ -35,6 +35,7 @@ + public function __construct(DeploymentConfig $deploymentConfig, JwkFactory $jwkFactory) + { + $this->keys = preg_split('/\s+/s', trim((string)$deploymentConfig->get('crypt/key'))); ++ $this->keys = [end($this->keys)]; + //Making sure keys are large enough. + foreach ($this->keys as &$key) { + $key = str_pad($key, 2048, '&', STR_PAD_BOTH); diff --git a/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.7.patch b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.7.patch new file mode 100644 index 00000000..4f23fb7f --- /dev/null +++ b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_CosmicSting__2.4.7.patch @@ -0,0 +1,55 @@ +diff --git a/vendor/magento/theme-adminhtml-backend/i18n/en_US.csv b/vendor/magento/theme-adminhtml-backend/i18n/en_US.csv +index 2708988e731..885d0056d4b 100644 +--- a/vendor/magento/theme-adminhtml-backend/i18n/en_US.csv ++++ b/vendor/magento/theme-adminhtml-backend/i18n/en_US.csv +@@ -547,3 +547,4 @@ Dashboard,Dashboard + "Web Section","Web Section" + "Store Email Addresses Section","Store Email Addresses Section" + "Email to a Friend","Email to a Friend" ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/theme-frontend-blank/i18n/en_US.csv b/vendor/magento/theme-frontend-blank/i18n/en_US.csv +index 025866f654d..cc02ab5ac90 100644 +--- a/vendor/magento/theme-frontend-blank/i18n/en_US.csv ++++ b/vendor/magento/theme-frontend-blank/i18n/en_US.csv +@@ -439,3 +439,4 @@ Summary,Summary + Test,Test + test,test + Two,Two ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/theme-frontend-luma/i18n/en_US.csv b/vendor/magento/theme-frontend-luma/i18n/en_US.csv +index e80cb58e679..3d0e8ab2650 100644 +--- a/vendor/magento/theme-frontend-luma/i18n/en_US.csv ++++ b/vendor/magento/theme-frontend-luma/i18n/en_US.csv +@@ -489,3 +489,4 @@ Remove,Remove + Test,Test + test,test + Two,Two ++"Invalid data type","Invalid data type" +diff --git a/vendor/magento/framework/Webapi/ServiceInputProcessor.php b/vendor/magento/framework/Webapi/ServiceInputProcessor.php +index cd7960409e1..df31058ff32 100644 +--- a/vendor/magento/framework/Webapi/ServiceInputProcessor.php ++++ b/vendor/magento/framework/Webapi/ServiceInputProcessor.php +@@ -278,6 +278,12 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface, ResetAf + // convert to string directly to avoid situations when $className is object + // which implements __toString method like \ReflectionObject + $className = (string) $className; ++ if (is_subclass_of($className, \SimpleXMLElement::class) ++ || is_subclass_of($className, \DOMElement::class)) { ++ throw new SerializationException( ++ new Phrase('Invalid data type') ++ ); ++ } + $class = new ClassReflection($className); + if (is_subclass_of($className, self::EXTENSION_ATTRIBUTES_TYPE)) { + $className = substr($className, 0, -strlen('Interface')); +diff --git a/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php b/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php +--- a/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php (revision 022e64b08a88658667bc2d6b922eada2b7910965) ++++ b/vendor/magento/module-jwt-user-token/Model/SecretBasedJwksFactory.php (revision 8d2b0c1c6b421cdcd7f62a48a5edc9b0211d92a2) +@@ -35,6 +35,7 @@ + public function __construct(DeploymentConfig $deploymentConfig, JwkFactory $jwkFactory) + { + $this->keys = preg_split('/\s+/s', trim((string)$deploymentConfig->get('crypt/key'))); ++ $this->keys = [end($this->keys)]; + //Making sure keys are large enough. + foreach ($this->keys as &$key) { + $key = str_pad($key, 2048, '&', STR_PAD_BOTH); diff --git a/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.4.patch b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.4.patch new file mode 100644 index 00000000..94adc617 --- /dev/null +++ b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.4.patch @@ -0,0 +1,163 @@ +diff --git a/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php b/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php +new file mode 100644 +index 0000000000000..8777f99139edc +--- /dev/null ++++ b/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php +@@ -0,0 +1,141 @@ ++encryptor = $encryptor; ++ $this->cache = $cache; ++ $this->writer = $writer; ++ $this->random = $random; ++ ++ parent::__construct(); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function configure() ++ { ++ $this->setName('encryption:key:change'); ++ $this->setDescription('Change the encryption key inside the env.php file.'); ++ $this->addOption( ++ 'key', ++ 'k', ++ InputOption::VALUE_OPTIONAL, ++ 'Key has to be a 32 characters long string. If not provided, a random key will be generated.' ++ ); ++ ++ parent::configure(); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function execute(InputInterface $input, OutputInterface $output) ++ { ++ try { ++ $key = $input->getOption('key'); ++ ++ if (!empty($key)) { ++ $this->encryptor->validateKey($key); ++ } ++ ++ $this->updateEncryptionKey($key); ++ $this->cache->clean(); ++ ++ $output->writeln('Encryption key has been updated successfully.'); ++ ++ return Cli::RETURN_SUCCESS; ++ } catch (\Exception $e) { ++ $output->writeln('' . $e->getMessage() . ''); ++ return Cli::RETURN_FAILURE; ++ } ++ } ++ ++ /** ++ * Update encryption key ++ * ++ * @param string|null $key ++ * @return void ++ * @throws FileSystemException ++ */ ++ private function updateEncryptionKey(string $key = null): void ++ { ++ // prepare new key, encryptor and new configuration segment ++ if (!$this->writer->checkIfWritable()) { ++ throw new FileSystemException(__('Deployment configuration file is not writable.')); ++ } ++ ++ if (null === $key) { ++ // md5() here is not for cryptographic use. It used for generate encryption key itself ++ // and do not encrypt any passwords ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $key = md5($this->random->getRandomString(ConfigOptionsListConstants::STORE_KEY_RANDOM_STRING_SIZE)); ++ } ++ ++ $this->encryptor->setNewKey($key); ++ ++ $encryptSegment = new ConfigData(ConfigFilePool::APP_ENV); ++ $encryptSegment->set(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY, $this->encryptor->exportKeys()); ++ ++ $configData = [$encryptSegment->getFileKey() => $encryptSegment->getData()]; ++ ++ $this->writer->saveConfig($configData); ++ } ++} +diff --git a/vendor/magento/module-encryption-key/etc/di.xml b/vendor/magento/module-encryption-key/etc/di.xml +index b4e471f4e40ef..495234759a7f8 100644 +--- a/vendor/magento/module-encryption-key/etc/di.xml ++++ b/vendor/magento/module-encryption-key/etc/di.xml +@@ -11,4 +11,11 @@ + Magento\Config\Model\Config\Structure\Proxy + + ++ ++ ++ ++ Magento\EncryptionKey\Console\Command\UpdateEncryptionKeyCommand ++ ++ ++ + diff --git a/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.5.patch b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.5.patch new file mode 100644 index 00000000..29adfc38 --- /dev/null +++ b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.5.patch @@ -0,0 +1,162 @@ +diff --git a/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php b/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php +new file mode 100644 +index 0000000000000..351379552e104 +--- /dev/null ++++ b/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php +@@ -0,0 +1,140 @@ ++encryptor = $encryptor; ++ $this->cache = $cache; ++ $this->writer = $writer; ++ $this->random = $random; ++ ++ parent::__construct(); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function configure() ++ { ++ $this->setName('encryption:key:change'); ++ $this->setDescription('Change the encryption key inside the env.php file.'); ++ $this->addOption( ++ 'key', ++ 'k', ++ InputOption::VALUE_OPTIONAL, ++ 'Key has to be a 32 characters long string. If not provided, a random key will be generated.' ++ ); ++ ++ parent::configure(); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function execute(InputInterface $input, OutputInterface $output) ++ { ++ try { ++ $key = $input->getOption('key'); ++ ++ if (!empty($key)) { ++ $this->encryptor->validateKey($key); ++ } ++ ++ $this->updateEncryptionKey($key); ++ $this->cache->clean(); ++ ++ $output->writeln('Encryption key has been updated successfully.'); ++ ++ return Cli::RETURN_SUCCESS; ++ } catch (\Exception $e) { ++ $output->writeln('' . $e->getMessage() . ''); ++ return Cli::RETURN_FAILURE; ++ } ++ } ++ ++ /** ++ * Update encryption key ++ * ++ * @param string|null $key ++ * @return void ++ * @throws FileSystemException ++ */ ++ private function updateEncryptionKey(string $key = null): void ++ { ++ // prepare new key, encryptor and new configuration segment ++ if (!$this->writer->checkIfWritable()) { ++ throw new FileSystemException(__('Deployment configuration file is not writable.')); ++ } ++ ++ if (null === $key) { ++ // md5() here is not for cryptographic use. It used for generate encryption key itself ++ // and do not encrypt any passwords ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $key = md5($this->random->getRandomString(ConfigOptionsListConstants::STORE_KEY_RANDOM_STRING_SIZE)); ++ } ++ $this->encryptor->setNewKey($key); ++ ++ $encryptSegment = new ConfigData(ConfigFilePool::APP_ENV); ++ $encryptSegment->set(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY, $this->encryptor->exportKeys()); ++ ++ $configData = [$encryptSegment->getFileKey() => $encryptSegment->getData()]; ++ ++ $this->writer->saveConfig($configData); ++ } ++} +diff --git a/vendor/magento/module-encryption-key/etc/di.xml b/vendor/magento/module-encryption-key/etc/di.xml +index b4e471f4e40ef..495234759a7f8 100644 +--- a/vendor/magento/module-encryption-key/etc/di.xml ++++ b/vendor/magento/module-encryption-key/etc/di.xml +@@ -11,4 +11,11 @@ + Magento\Config\Model\Config\Structure\Proxy + + ++ ++ ++ ++ Magento\EncryptionKey\Console\Command\UpdateEncryptionKeyCommand ++ ++ ++ + diff --git a/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.6.patch b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.6.patch new file mode 100644 index 00000000..c5ba4386 --- /dev/null +++ b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.6.patch @@ -0,0 +1,159 @@ +diff --git a/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php b/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php +new file mode 100644 +index 0000000000000..0e4995b847893 +--- /dev/null ++++ b/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php +@@ -0,0 +1,137 @@ ++encryptor = $encryptor; ++ $this->cache = $cache; ++ $this->writer = $writer; ++ $this->random = $random; ++ ++ parent::__construct(); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function configure() ++ { ++ $this->setName('encryption:key:change'); ++ $this->setDescription('Change the encryption key inside the env.php file.'); ++ $this->addOption( ++ 'key', ++ 'k', ++ InputOption::VALUE_OPTIONAL, ++ 'Key has to be a 32 characters long string. If not provided, a random key will be generated.' ++ ); ++ ++ parent::configure(); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function execute(InputInterface $input, OutputInterface $output) ++ { ++ try { ++ $key = $input->getOption('key'); ++ ++ if (!empty($key)) { ++ $this->encryptor->validateKey($key); ++ } ++ ++ $this->updateEncryptionKey($key); ++ $this->cache->clean(); ++ ++ $output->writeln('Encryption key has been updated successfully.'); ++ ++ return Command::SUCCESS; ++ } catch (\Exception $e) { ++ $output->writeln('' . $e->getMessage() . ''); ++ return Command::FAILURE; ++ } ++ } ++ ++ /** ++ * Update encryption key ++ * ++ * @param string|null $key ++ * @return void ++ * @throws FileSystemException ++ */ ++ private function updateEncryptionKey(string $key = null): void ++ { ++ // prepare new key, encryptor and new configuration segment ++ if (!$this->writer->checkIfWritable()) { ++ throw new FileSystemException(__('Deployment configuration file is not writable.')); ++ } ++ ++ if (null === $key) { ++ // md5() here is not for cryptographic use. It used for generate encryption key itself ++ // and do not encrypt any passwords ++ // phpcs:ignore Magento2.Security.InsecureFunction ++ $key = md5($this->random->getRandomString(ConfigOptionsListConstants::STORE_KEY_RANDOM_STRING_SIZE)); ++ } ++ ++ $this->encryptor->setNewKey($key); ++ ++ $encryptSegment = new ConfigData(ConfigFilePool::APP_ENV); ++ $encryptSegment->set(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY, $this->encryptor->exportKeys()); ++ ++ $configData = [$encryptSegment->getFileKey() => $encryptSegment->getData()]; ++ ++ $this->writer->saveConfig($configData); ++ } ++} +diff --git a/vendor/magento/module-encryption-key/etc/di.xml b/vendor/magento/module-encryption-key/etc/di.xml +index b4e471f4e40ef..495234759a7f8 100644 +--- a/vendor/magento/module-encryption-key/etc/di.xml ++++ b/vendor/magento/module-encryption-key/etc/di.xml +@@ -11,4 +11,11 @@ + Magento\Config\Model\Config\Structure\Proxy + + ++ ++ ++ ++ Magento\EncryptionKey\Console\Command\UpdateEncryptionKeyCommand ++ ++ ++ + diff --git a/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.7.patch b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.7.patch new file mode 100644 index 00000000..0c8e1fca --- /dev/null +++ b/patches/MCLOUD-12969__Patch_for_CVE_2024_34102_KeyRotation__2.4.7.patch @@ -0,0 +1,157 @@ +diff --git a/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php b/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php +new file mode 100644 +index 0000000000000..cd6ffb4323163 +--- /dev/null ++++ b/vendor/magento/module-encryption-key/Console/Command/UpdateEncryptionKeyCommand.php +@@ -0,0 +1,135 @@ ++encryptor = $encryptor; ++ $this->cache = $cache; ++ $this->writer = $writer; ++ $this->random = $random; ++ ++ parent::__construct(); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function configure() ++ { ++ $this->setName('encryption:key:change'); ++ $this->setDescription('Change the encryption key inside the env.php file.'); ++ $this->addOption( ++ 'key', ++ 'k', ++ InputOption::VALUE_OPTIONAL, ++ 'Key has to be a 32 characters long string. If not provided, a random key will be generated.' ++ ); ++ ++ parent::configure(); ++ } ++ ++ /** ++ * @inheritDoc ++ */ ++ protected function execute(InputInterface $input, OutputInterface $output) ++ { ++ try { ++ $key = $input->getOption('key'); ++ ++ if (!empty($key)) { ++ $this->encryptor->validateKey($key); ++ } ++ ++ $this->updateEncryptionKey($key); ++ $this->cache->clean(); ++ ++ $output->writeln('Encryption key has been updated successfully.'); ++ ++ return Command::SUCCESS; ++ } catch (\Exception $e) { ++ $output->writeln('' . $e->getMessage() . ''); ++ return Command::FAILURE; ++ } ++ } ++ ++ /** ++ * Update encryption key ++ * ++ * @param string|null $key ++ * @return void ++ * @throws FileSystemException ++ */ ++ private function updateEncryptionKey(string $key = null): void ++ { ++ // prepare new key, encryptor and new configuration segment ++ if (!$this->writer->checkIfWritable()) { ++ throw new FileSystemException(__('Deployment configuration file is not writable.')); ++ } ++ ++ if (null === $key) { ++ $key = ConfigOptionsListConstants::STORE_KEY_ENCODED_RANDOM_STRING_PREFIX . ++ $this->random->getRandomBytes(ConfigOptionsListConstants::STORE_KEY_RANDOM_STRING_SIZE); ++ } ++ ++ $this->encryptor->setNewKey($key); ++ ++ $encryptSegment = new ConfigData(ConfigFilePool::APP_ENV); ++ $encryptSegment->set(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY, $this->encryptor->exportKeys()); ++ ++ $configData = [$encryptSegment->getFileKey() => $encryptSegment->getData()]; ++ ++ $this->writer->saveConfig($configData); ++ } ++} +diff --git a/vendor/magento/module-encryption-key/etc/di.xml b/vendor/magento/module-encryption-key/etc/di.xml +index b4e471f4e40ef..495234759a7f8 100644 +--- a/vendor/magento/module-encryption-key/etc/di.xml ++++ b/vendor/magento/module-encryption-key/etc/di.xml +@@ -11,4 +11,11 @@ + Magento\Config\Model\Config\Structure\Proxy + + ++ ++ ++ ++ Magento\EncryptionKey\Console\Command\UpdateEncryptionKeyCommand ++ ++ ++ + diff --git a/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.4.patch b/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.4.patch new file mode 100644 index 00000000..fe8f0d19 --- /dev/null +++ b/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.4.patch @@ -0,0 +1,1096 @@ +diff --git a/vendor/magento/module-customer/Model/AccountManagement.php b/vendor/magento/module-customer/Model/AccountManagement.php +index 6e0aac11d8e9..27c2bf4051cc 100644 +--- a/vendor/magento/module-customer/Model/AccountManagement.php ++++ b/vendor/magento/module-customer/Model/AccountManagement.php +@@ -876,11 +876,6 @@ public function getConfirmationStatus($customerId) + */ + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + { +- $groupId = $customer->getGroupId(); +- if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { +- $customer->setGroupId(null); +- } +- + if ($password !== null) { + $this->checkPasswordStrength($password); + $customerEmail = $customer->getEmail(); +diff --git a/vendor/magento/module-customer/Model/AccountManagementApi.php b/vendor/magento/module-customer/Model/AccountManagementApi.php +index 02a05705b57e..8b4f78ab26c7 100644 +--- a/vendor/magento/module-customer/Model/AccountManagementApi.php ++++ b/vendor/magento/module-customer/Model/AccountManagementApi.php +@@ -6,16 +6,127 @@ + + namespace Magento\Customer\Model; + ++use Magento\Customer\Api\AddressRepositoryInterface; ++use Magento\Customer\Api\CustomerMetadataInterface; ++use Magento\Customer\Api\CustomerRepositoryInterface; + use Magento\Customer\Api\Data\CustomerInterface; ++use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; ++use Magento\Customer\Helper\View as CustomerViewHelper; ++use Magento\Customer\Model\Config\Share as ConfigShare; ++use Magento\Customer\Model\Customer as CustomerModel; ++use Magento\Customer\Model\Metadata\Validator; ++use Magento\Framework\Api\ExtensibleDataObjectConverter; ++use Magento\Framework\App\Config\ScopeConfigInterface; ++use Magento\Framework\AuthorizationInterface; ++use Magento\Framework\DataObjectFactory as ObjectFactory; ++use Magento\Framework\Encryption\EncryptorInterface as Encryptor; ++use Magento\Framework\Event\ManagerInterface; ++use Magento\Framework\Exception\AuthorizationException; ++use Magento\Framework\Mail\Template\TransportBuilder; ++use Magento\Framework\Math\Random; ++use Magento\Framework\Reflection\DataObjectProcessor; ++use Magento\Framework\Registry; ++use Magento\Framework\Stdlib\DateTime; ++use Magento\Framework\Stdlib\StringUtils as StringHelper; ++use Magento\Store\Model\StoreManagerInterface; ++use Psr\Log\LoggerInterface as PsrLogger; + + /** + * Account Management service implementation for external API access. ++ * + * Handle various customer account actions. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + class AccountManagementApi extends AccountManagement + { ++ /** ++ * @var AuthorizationInterface ++ */ ++ private $authorization; ++ ++ /** ++ * @param CustomerFactory $customerFactory ++ * @param ManagerInterface $eventManager ++ * @param StoreManagerInterface $storeManager ++ * @param Random $mathRandom ++ * @param Validator $validator ++ * @param ValidationResultsInterfaceFactory $validationResultsDataFactory ++ * @param AddressRepositoryInterface $addressRepository ++ * @param CustomerMetadataInterface $customerMetadataService ++ * @param CustomerRegistry $customerRegistry ++ * @param PsrLogger $logger ++ * @param Encryptor $encryptor ++ * @param ConfigShare $configShare ++ * @param StringHelper $stringHelper ++ * @param CustomerRepositoryInterface $customerRepository ++ * @param ScopeConfigInterface $scopeConfig ++ * @param TransportBuilder $transportBuilder ++ * @param DataObjectProcessor $dataProcessor ++ * @param Registry $registry ++ * @param CustomerViewHelper $customerViewHelper ++ * @param DateTime $dateTime ++ * @param CustomerModel $customerModel ++ * @param ObjectFactory $objectFactory ++ * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter ++ * @param AuthorizationInterface $authorization ++ * @SuppressWarnings(PHPMD.ExcessiveParameterList) ++ */ ++ public function __construct( ++ CustomerFactory $customerFactory, ++ ManagerInterface $eventManager, ++ StoreManagerInterface $storeManager, ++ Random $mathRandom, ++ Validator $validator, ++ ValidationResultsInterfaceFactory $validationResultsDataFactory, ++ AddressRepositoryInterface $addressRepository, ++ CustomerMetadataInterface $customerMetadataService, ++ CustomerRegistry $customerRegistry, ++ PsrLogger $logger, ++ Encryptor $encryptor, ++ ConfigShare $configShare, ++ StringHelper $stringHelper, ++ CustomerRepositoryInterface $customerRepository, ++ ScopeConfigInterface $scopeConfig, ++ TransportBuilder $transportBuilder, ++ DataObjectProcessor $dataProcessor, ++ Registry $registry, ++ CustomerViewHelper $customerViewHelper, ++ DateTime $dateTime, ++ CustomerModel $customerModel, ++ ObjectFactory $objectFactory, ++ ExtensibleDataObjectConverter $extensibleDataObjectConverter, ++ AuthorizationInterface $authorization ++ ) { ++ $this->authorization = $authorization; ++ parent::__construct( ++ $customerFactory, ++ $eventManager, ++ $storeManager, ++ $mathRandom, ++ $validator, ++ $validationResultsDataFactory, ++ $addressRepository, ++ $customerMetadataService, ++ $customerRegistry, ++ $logger, ++ $encryptor, ++ $configShare, ++ $stringHelper, ++ $customerRepository, ++ $scopeConfig, ++ $transportBuilder, ++ $dataProcessor, ++ $registry, ++ $customerViewHelper, ++ $dateTime, ++ $customerModel, ++ $objectFactory, ++ $extensibleDataObjectConverter ++ ); ++ } ++ + /** + * @inheritDoc + * +@@ -23,9 +134,30 @@ class AccountManagementApi extends AccountManagement + */ + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + { ++ $this->validateCustomerRequest($customer); + $customer = parent::createAccount($customer, $password, $redirectUrl); + $customer->setConfirmation(null); + + return $customer; + } ++ ++ /** ++ * Validate anonymous request ++ * ++ * @param CustomerInterface $customer ++ * @return void ++ * @throws AuthorizationException ++ */ ++ private function validateCustomerRequest(CustomerInterface $customer): void ++ { ++ $groupId = $customer->getGroupId(); ++ if (isset($groupId) && ++ !$this->authorization->isAllowed(self::ADMIN_RESOURCE) ++ ) { ++ $params = ['resources' => self::ADMIN_RESOURCE]; ++ throw new AuthorizationException( ++ __("The consumer isn't authorized to access %resources.", $params) ++ ); ++ } ++ } + } +diff --git a/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php b/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php +new file mode 100644 +index 000000000000..295b33d2db14 +--- /dev/null ++++ b/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php +@@ -0,0 +1,89 @@ ++authorization = $authorization; ++ } ++ ++ /** ++ * Validate groupId for anonymous request ++ * ++ * @param MassSchedule $massSchedule ++ * @param string $topic ++ * @param array $entitiesArray ++ * @param string|null $groupId ++ * @param string|null $userId ++ * @return null ++ * @throws AuthorizationException ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforePublishMass( ++ MassSchedule $massSchedule, ++ string $topic, ++ array $entitiesArray, ++ string $groupId = null, ++ string $userId = null ++ ) { ++ // only apply the plugin on account create. ++ if ($topic !== self::TOPIC_NAME) { ++ return; ++ } ++ ++ foreach ($entitiesArray as $entityParams) { ++ foreach ($entityParams as $entity) { ++ if ($entity instanceof CustomerInterface) { ++ $groupId = $entity->getGroupId(); ++ if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { ++ $params = ['resources' => self::ADMIN_RESOURCE]; ++ throw new AuthorizationException( ++ __("The consumer isn't authorized to access %resources.", $params) ++ ); ++ } ++ } ++ } ++ } ++ return null; ++ } ++} +diff --git a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php +new file mode 100644 +index 000000000000..074d40021a18 +--- /dev/null ++++ b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php +@@ -0,0 +1,421 @@ ++customerFactory = $this->createPartialMock(CustomerFactory::class, ['create']); ++ $this->manager = $this->getMockForAbstractClass(ManagerInterface::class); ++ $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); ++ $this->random = $this->createMock(Random::class); ++ $this->validator = $this->createMock(Validator::class); ++ $this->validationResultsInterfaceFactory = $this->createMock( ++ ValidationResultsInterfaceFactory::class ++ ); ++ $this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class); ++ $this->customerMetadata = $this->getMockForAbstractClass(CustomerMetadataInterface::class); ++ $this->customerRegistry = $this->createMock(CustomerRegistry::class); ++ ++ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); ++ $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class); ++ $this->share = $this->createMock(Share::class); ++ $this->string = $this->createMock(StringUtils::class); ++ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); ++ $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->transportBuilder = $this->createMock(TransportBuilder::class); ++ $this->dataObjectProcessor = $this->createMock(DataObjectProcessor::class); ++ $this->registry = $this->createMock(Registry::class); ++ $this->customerViewHelper = $this->createMock(View::class); ++ $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); ++ $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class); ++ $this->objectFactory = $this->createMock(DataObjectFactory::class); ++ $this->addressRegistryMock = $this->createMock(AddressRegistry::class); ++ $this->extensibleDataObjectConverter = $this->createMock( ++ ExtensibleDataObjectConverter::class ++ ); ++ $this->allowedCountriesReader = $this->createMock(AllowedCountries::class); ++ $this->customerSecure = $this->getMockBuilder(CustomerSecure::class) ++ ->onlyMethods(['addData', 'setData']) ++ ->addMethods(['setRpToken', 'setRpTokenCreatedAt']) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $this->dateTimeFactory = $this->createMock(DateTimeFactory::class); ++ $this->accountConfirmation = $this->createMock(AccountConfirmation::class); ++ $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); ++ ++ $this->visitorCollectionFactory = $this->getMockBuilder(CollectionFactory::class) ++ ->disableOriginalConstructor() ++ ->onlyMethods(['create']) ++ ->getMock(); ++ $this->sessionManager = $this->getMockBuilder(SessionManagerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->saveHandler = $this->getMockBuilder(SaveHandlerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->authorizationMock = $this->createMock(Authorization::class); ++ $this->objectManagerHelper = new ObjectManagerHelper($this); ++ $this->accountManagement = $this->objectManagerHelper->getObject( ++ AccountManagementApi::class, ++ [ ++ 'customerFactory' => $this->customerFactory, ++ 'eventManager' => $this->manager, ++ 'storeManager' => $this->storeManager, ++ 'mathRandom' => $this->random, ++ 'validator' => $this->validator, ++ 'validationResultsDataFactory' => $this->validationResultsInterfaceFactory, ++ 'addressRepository' => $this->addressRepository, ++ 'customerMetadataService' => $this->customerMetadata, ++ 'customerRegistry' => $this->customerRegistry, ++ 'logger' => $this->logger, ++ 'encryptor' => $this->encryptor, ++ 'configShare' => $this->share, ++ 'stringHelper' => $this->string, ++ 'customerRepository' => $this->customerRepository, ++ 'scopeConfig' => $this->scopeConfig, ++ 'transportBuilder' => $this->transportBuilder, ++ 'dataProcessor' => $this->dataObjectProcessor, ++ 'registry' => $this->registry, ++ 'customerViewHelper' => $this->customerViewHelper, ++ 'dateTime' => $this->dateTime, ++ 'customerModel' => $this->customer, ++ 'objectFactory' => $this->objectFactory, ++ 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, ++ 'dateTimeFactory' => $this->dateTimeFactory, ++ 'accountConfirmation' => $this->accountConfirmation, ++ 'sessionManager' => $this->sessionManager, ++ 'saveHandler' => $this->saveHandler, ++ 'visitorCollectionFactory' => $this->visitorCollectionFactory, ++ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, ++ 'addressRegistry' => $this->addressRegistryMock, ++ 'allowedCountriesReader' => $this->allowedCountriesReader, ++ 'authorization' => $this->authorizationMock ++ ] ++ ); ++ $this->accountManagementMock = $this->createMock(AccountManagement::class); ++ ++ $this->storeMock = $this->getMockBuilder( ++ StoreInterface::class ++ )->disableOriginalConstructor() ++ ->getMock(); ++ } ++ ++ /** ++ * Verify that only authorized request will be able to change groupId ++ * ++ * @param int $groupId ++ * @param int $customerId ++ * @param bool $isAllowed ++ * @param int $willThrowException ++ * @return void ++ * @throws AuthorizationException ++ * @throws LocalizedException ++ * @dataProvider customerDataProvider ++ */ ++ public function testBeforeCreateAccount( ++ int $groupId, ++ int $customerId, ++ bool $isAllowed, ++ int $willThrowException ++ ): void { ++ if ($willThrowException) { ++ $this->expectException(AuthorizationException::class); ++ } else { ++ $this->expectNotToPerformAssertions(); ++ } ++ $this->authorizationMock ++ ->expects($this->once()) ++ ->method('isAllowed') ++ ->with('Magento_Customer::manage') ++ ->willReturn($isAllowed); ++ ++ $customer = $this->getMockBuilder(CustomerInterface::class) ++ ->addMethods(['setData']) ++ ->getMockForAbstractClass(); ++ $customer->method('getGroupId')->willReturn($groupId); ++ $customer->method('getId')->willReturn($customerId); ++ $customer->method('getWebsiteId')->willReturn(2); ++ $customer->method('getStoreId')->willReturn(1); ++ $customer->method('setData')->willReturn(1); ++ ++ $this->customerRepository->method('get')->willReturn($customer); ++ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); ++ $this->customerRepository->method('save')->willReturn($customer); ++ ++ if (!$willThrowException) { ++ $this->accountManagementMock->method('createAccountWithPasswordHash')->willReturn($customer); ++ $this->storeMock->expects($this->any())->method('getId')->willReturnOnConsecutiveCalls(2, 1); ++ $this->random->method('getUniqueHash')->willReturn('testabc'); ++ $date = $this->getMockBuilder(\DateTime::class) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $this->dateTimeFactory->expects(static::once()) ++ ->method('create') ++ ->willReturn($date); ++ $date->expects(static::once()) ++ ->method('format') ++ ->with('Y-m-d H:i:s') ++ ->willReturn('2015-01-01 00:00:00'); ++ $this->customerRegistry->method('retrieveSecureData')->willReturn($this->customerSecure); ++ $this->storeManager->method('getStores') ++ ->willReturn([$this->storeMock]); ++ } ++ $this->accountManagement->createAccount($customer); ++ } ++ ++ /** ++ * @return array ++ */ ++ public function customerDataProvider(): array ++ { ++ return [ ++ [3, 1, false, 1], ++ [3, 1, true, 0] ++ ]; ++ } ++} +diff --git a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php +index 8ff6a8585212..cbe0a18e4b17 100644 +--- a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php ++++ b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php +@@ -1222,7 +1222,6 @@ public function testCreateAccountWithGroupId(): void + $minPasswordLength = 5; + $minCharacterSetsNum = 2; + $defaultGroupId = 1; +- $requestedGroupId = 3; + + $datetime = $this->prepareDateTimeFactory(); + +@@ -1299,9 +1298,6 @@ public function testCreateAccountWithGroupId(): void + return null; + } + })); +- $customer->expects($this->atLeastOnce()) +- ->method('getGroupId') +- ->willReturn($requestedGroupId); + $customer + ->method('setGroupId') + ->willReturnOnConsecutiveCalls(null, $defaultGroupId); +diff --git a/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php b/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php +new file mode 100644 +index 000000000000..107df2c2863e +--- /dev/null ++++ b/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php +@@ -0,0 +1,112 @@ ++authorizationMock = $this->createMock(Authorization::class); ++ $this->plugin = $objectManager->getObject(AsyncRequestCustomerGroupAuthorization::class, [ ++ 'authorization' => $this->authorizationMock ++ ]); ++ $this->massScheduleMock = $this->createMock(MassSchedule::class); ++ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); ++ } ++ ++ /** ++ * Verify that only authorized request will be able to change groupId ++ * ++ * @param int $groupId ++ * @param int $customerId ++ * @param bool $isAllowed ++ * @param int $willThrowException ++ * @return void ++ * @throws AuthorizationException ++ * @dataProvider customerDataProvider ++ */ ++ public function testBeforePublishMass( ++ int $groupId, ++ int $customerId, ++ bool $isAllowed, ++ int $willThrowException ++ ): void { ++ if ($willThrowException) { ++ $this->expectException(AuthorizationException::class); ++ } else { ++ $this->expectNotToPerformAssertions(); ++ } ++ $customer = $this->getMockForAbstractClass(CustomerInterface::class); ++ $customer->method('getGroupId')->willReturn($groupId); ++ $customer->method('getId')->willReturn($customerId); ++ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); ++ $entitiesArray = [ ++ [$customer, 'Password1', ''] ++ ]; ++ $this->authorizationMock ++ ->expects($this->once()) ++ ->method('isAllowed') ++ ->with('Magento_Customer::manage') ++ ->willReturn($isAllowed); ++ $this->plugin->beforePublishMass( ++ $this->massScheduleMock, ++ 'async.magento.customer.api.accountmanagementinterface.createaccount.post', ++ $entitiesArray, ++ '', ++ '' ++ ); ++ } ++ ++ /** ++ * @return array ++ */ ++ public function customerDataProvider(): array ++ { ++ return [ ++ [3, 1, false, 1], ++ [3, 1, true, 0] ++ ]; ++ } ++} +diff --git a/vendor/magento/module-customer/composer.json b/vendor/magento/module-customer/composer.json +index 2d76da56bff7..ff34d423c2da 100644 +--- a/vendor/magento/module-customer/composer.json ++++ b/vendor/magento/module-customer/composer.json +@@ -29,5 +29,6 @@ + "suggest": { + "magento/module-cookie": "100.4.*", + "magento/module-customer-sample-data": "Sample Data version: 100.4.*", +- "magento/module-webapi": "100.4.*" ++ "magento/module-webapi": "100.4.*", ++ "magento/module-asynchronous-operations": "100.4.*" + }, +diff --git a/vendor/magento/module-customer/etc/di.xml b/vendor/magento/module-customer/etc/di.xml +index 156986b7b4a3..120a8dda8aec 100644 +--- a/vendor/magento/module-customer/etc/di.xml ++++ b/vendor/magento/module-customer/etc/di.xml +@@ -560,4 +560,9 @@ + + + ++ ++ ++ + +diff --git a/vendor/magento/module-quote/etc/webapi.xml b/vendor/magento/module-quote/etc/webapi.xml +index 79d98968ea19..a7cce5b03a26 100644 +--- a/vendor/magento/module-quote/etc/webapi.xml ++++ b/vendor/magento/module-quote/etc/webapi.xml +@@ -98,6 +98,9 @@ + + + ++ ++ %customer_id% ++ + + + +diff --git a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +index 8601e5011bda..93555559ac9a 100644 +--- a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php ++++ b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +@@ -8,10 +8,12 @@ + + namespace Magento\WebapiAsync\Controller\Rest\Asynchronous; + ++use Magento\Framework\Api\SimpleDataObjectConverter; + use Magento\Framework\App\ObjectManager; + use Magento\Framework\Exception\AuthorizationException; + use Magento\Framework\Exception\InputException; + use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Reflection\MethodsMap; + use Magento\Framework\Webapi\Exception; + use Magento\Framework\Webapi\Rest\Request as RestRequest; + use Magento\Framework\Webapi\ServiceInputProcessor; +@@ -24,6 +26,8 @@ + + /** + * This class is responsible for retrieving resolved input data ++ * ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + class InputParamsResolver + { +@@ -61,6 +65,11 @@ class InputParamsResolver + */ + private $inputArraySizeLimitValue; + ++ /** ++ * @var MethodsMap ++ */ ++ private $methodsMap; ++ + /** + * Initialize dependencies. + * +@@ -72,6 +81,7 @@ class InputParamsResolver + * @param WebapiInputParamsResolver $inputParamsResolver + * @param bool $isBulk + * @param InputArraySizeLimitValue|null $inputArraySizeLimitValue ++ * @param MethodsMap|null $methodsMap + */ + public function __construct( + RestRequest $request, +@@ -81,7 +91,8 @@ public function __construct( + RequestValidator $requestValidator, + WebapiInputParamsResolver $inputParamsResolver, + bool $isBulk = false, +- ?InputArraySizeLimitValue $inputArraySizeLimitValue = null ++ ?InputArraySizeLimitValue $inputArraySizeLimitValue = null, ++ ?MethodsMap $methodsMap = null + ) { + $this->request = $request; + $this->paramsOverrider = $paramsOverrider; +@@ -92,6 +103,8 @@ public function __construct( + $this->isBulk = $isBulk; + $this->inputArraySizeLimitValue = $inputArraySizeLimitValue ?? ObjectManager::getInstance() + ->get(InputArraySizeLimitValue::class); ++ $this->methodsMap = $methodsMap ?? ObjectManager::getInstance() ++ ->get(MethodsMap::class); + } + + /** +@@ -113,12 +126,19 @@ public function resolve() + + $this->requestValidator->validate(); + $webapiResolvedParams = []; ++ $inputData = $this->getInputData(); + $route = $this->getRoute(); + $routeServiceClass = $route->getServiceClass(); + $routeServiceMethod = $route->getServiceMethod(); + $this->inputArraySizeLimitValue->set($route->getInputArraySizeLimit()); + +- foreach ($this->getInputData() as $key => $singleEntityParams) { ++ $this->validateParameters($routeServiceClass, $routeServiceMethod, array_keys($route->getParameters())); ++ ++ foreach ($inputData as $key => $singleEntityParams) { ++ if (!is_array($singleEntityParams)) { ++ continue; ++ } ++ + $webapiResolvedParams[$key] = $this->resolveBulkItemParams( + $singleEntityParams, + $routeServiceClass, +@@ -142,11 +162,22 @@ public function getInputData() + $inputData = $this->request->getRequestData(); + + $httpMethod = $this->request->getHttpMethod(); +- if ($httpMethod == RestRequest::HTTP_METHOD_DELETE) { ++ if ($httpMethod === RestRequest::HTTP_METHOD_DELETE) { + $requestBodyParams = $this->request->getBodyParams(); + $inputData = array_merge($requestBodyParams, $inputData); + } +- return $inputData; ++ ++ return array_map(function ($singleEntityParams) { ++ if (is_array($singleEntityParams)) { ++ $singleEntityParams = $this->filterInputData($singleEntityParams); ++ $singleEntityParams = $this->paramsOverrider->override( ++ $singleEntityParams, ++ $this->getRoute()->getParameters() ++ ); ++ } ++ ++ return $singleEntityParams; ++ }, $inputData); + } + + /** +@@ -179,4 +210,64 @@ private function resolveBulkItemParams(array $inputData, string $serviceClass, s + { + return $this->serviceInputProcessor->process($serviceClass, $serviceMethod, $inputData); + } ++ ++ /** ++ * Validates InputData ++ * ++ * @param array $inputData ++ * @return array ++ */ ++ private function filterInputData(array $inputData): array ++ { ++ $result = []; ++ ++ $data = array_filter($inputData, function ($k) use (&$result) { ++ $key = is_string($k) ? strtolower(str_replace('_', "", $k)) : $k; ++ return !isset($result[$key]) && ($result[$key] = true); ++ }, ARRAY_FILTER_USE_KEY); ++ ++ return array_map(function ($value) { ++ return is_array($value) ? $this->filterInputData($value) : $value; ++ }, $data); ++ } ++ ++ /** ++ * Validate that parameters are really used in the current request. ++ * ++ * @param string $serviceClassName ++ * @param string $serviceMethodName ++ * @param array $paramOverriders ++ */ ++ private function validateParameters( ++ string $serviceClassName, ++ string $serviceMethodName, ++ array $paramOverriders ++ ): void { ++ $methodParams = $this->methodsMap->getMethodParams($serviceClassName, $serviceMethodName); ++ foreach ($paramOverriders as $key => $param) { ++ $arrayKeys = explode('.', $param ?? ''); ++ $value = array_shift($arrayKeys); ++ ++ foreach ($methodParams as $serviceMethodParam) { ++ $serviceMethodParamName = $serviceMethodParam[MethodsMap::METHOD_META_NAME]; ++ $serviceMethodType = $serviceMethodParam[MethodsMap::METHOD_META_TYPE]; ++ ++ $camelCaseValue = SimpleDataObjectConverter::snakeCaseToCamelCase($value); ++ if ($serviceMethodParamName === $value || $serviceMethodParamName === $camelCaseValue) { ++ if (count($arrayKeys) > 0) { ++ $camelCaseKey = SimpleDataObjectConverter::snakeCaseToCamelCase('set_' . $arrayKeys[0]); ++ $this->validateParameters($serviceMethodType, $camelCaseKey, [implode('.', $arrayKeys)]); ++ } ++ unset($paramOverriders[$key]); ++ break; ++ } ++ } ++ } ++ ++ if (!empty($paramOverriders)) { ++ $message = 'The current request does not expect the next parameters: ' ++ . implode(', ', $paramOverriders); ++ throw new \UnexpectedValueException(__($message)->__toString()); ++ } ++ } + } +diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +index ce9e4ee94178..e08fe0388cfb 100644 +--- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php ++++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +@@ -10,10 +10,13 @@ + + class GuestCartManagementTest extends WebapiAbstract + { +- const SERVICE_VERSION = 'V1'; +- const SERVICE_NAME = 'quoteGuestCartManagementV1'; +- const RESOURCE_PATH = '/V1/guest-carts/'; ++ public const SERVICE_VERSION = 'V1'; ++ public const SERVICE_NAME = 'quoteGuestCartManagementV1'; ++ public const RESOURCE_PATH = '/V1/guest-carts/'; + ++ /** ++ * @var array List of created quotes ++ */ + protected $createdQuotes = []; + + /** +@@ -339,7 +342,7 @@ public function testPlaceOrder() + public function testAssignCustomerByGuestUser() + { + $this->expectException(\Exception::class); +- $this->expectExceptionMessage('You don\'t have the correct permissions to assign the customer to the cart.'); ++ $this->expectExceptionMessage('Enter and try again.'); + + /** @var $quote \Magento\Quote\Model\Quote */ + $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); +diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +index efc7e669b360..18ffe842c794 100644 +--- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt ++++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +@@ -109,3 +109,4 @@ app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/ + app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php + app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php + app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml ++app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php diff --git a/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.5.patch b/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.5.patch new file mode 100644 index 00000000..53e4e8ea --- /dev/null +++ b/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.5.patch @@ -0,0 +1,1092 @@ +diff --git a/vendor/magento/module-customer/Model/AccountManagement.php b/vendor/magento/module-customer/Model/AccountManagement.php +index d70058aef445..513c0b0717e8 100644 +--- a/vendor/magento/module-customer/Model/AccountManagement.php ++++ b/vendor/magento/module-customer/Model/AccountManagement.php +@@ -879,11 +879,6 @@ public function getConfirmationStatus($customerId) + */ + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + { +- $groupId = $customer->getGroupId(); +- if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { +- $customer->setGroupId(null); +- } +- + if ($password !== null) { + $this->checkPasswordStrength($password); + $customerEmail = $customer->getEmail(); +diff --git a/vendor/magento/module-customer/Model/AccountManagementApi.php b/vendor/magento/module-customer/Model/AccountManagementApi.php +index 02a05705b57e..8b4f78ab26c7 100644 +--- a/vendor/magento/module-customer/Model/AccountManagementApi.php ++++ b/vendor/magento/module-customer/Model/AccountManagementApi.php +@@ -6,16 +6,127 @@ + + namespace Magento\Customer\Model; + ++use Magento\Customer\Api\AddressRepositoryInterface; ++use Magento\Customer\Api\CustomerMetadataInterface; ++use Magento\Customer\Api\CustomerRepositoryInterface; + use Magento\Customer\Api\Data\CustomerInterface; ++use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; ++use Magento\Customer\Helper\View as CustomerViewHelper; ++use Magento\Customer\Model\Config\Share as ConfigShare; ++use Magento\Customer\Model\Customer as CustomerModel; ++use Magento\Customer\Model\Metadata\Validator; ++use Magento\Framework\Api\ExtensibleDataObjectConverter; ++use Magento\Framework\App\Config\ScopeConfigInterface; ++use Magento\Framework\AuthorizationInterface; ++use Magento\Framework\DataObjectFactory as ObjectFactory; ++use Magento\Framework\Encryption\EncryptorInterface as Encryptor; ++use Magento\Framework\Event\ManagerInterface; ++use Magento\Framework\Exception\AuthorizationException; ++use Magento\Framework\Mail\Template\TransportBuilder; ++use Magento\Framework\Math\Random; ++use Magento\Framework\Reflection\DataObjectProcessor; ++use Magento\Framework\Registry; ++use Magento\Framework\Stdlib\DateTime; ++use Magento\Framework\Stdlib\StringUtils as StringHelper; ++use Magento\Store\Model\StoreManagerInterface; ++use Psr\Log\LoggerInterface as PsrLogger; + + /** + * Account Management service implementation for external API access. ++ * + * Handle various customer account actions. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + class AccountManagementApi extends AccountManagement + { ++ /** ++ * @var AuthorizationInterface ++ */ ++ private $authorization; ++ ++ /** ++ * @param CustomerFactory $customerFactory ++ * @param ManagerInterface $eventManager ++ * @param StoreManagerInterface $storeManager ++ * @param Random $mathRandom ++ * @param Validator $validator ++ * @param ValidationResultsInterfaceFactory $validationResultsDataFactory ++ * @param AddressRepositoryInterface $addressRepository ++ * @param CustomerMetadataInterface $customerMetadataService ++ * @param CustomerRegistry $customerRegistry ++ * @param PsrLogger $logger ++ * @param Encryptor $encryptor ++ * @param ConfigShare $configShare ++ * @param StringHelper $stringHelper ++ * @param CustomerRepositoryInterface $customerRepository ++ * @param ScopeConfigInterface $scopeConfig ++ * @param TransportBuilder $transportBuilder ++ * @param DataObjectProcessor $dataProcessor ++ * @param Registry $registry ++ * @param CustomerViewHelper $customerViewHelper ++ * @param DateTime $dateTime ++ * @param CustomerModel $customerModel ++ * @param ObjectFactory $objectFactory ++ * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter ++ * @param AuthorizationInterface $authorization ++ * @SuppressWarnings(PHPMD.ExcessiveParameterList) ++ */ ++ public function __construct( ++ CustomerFactory $customerFactory, ++ ManagerInterface $eventManager, ++ StoreManagerInterface $storeManager, ++ Random $mathRandom, ++ Validator $validator, ++ ValidationResultsInterfaceFactory $validationResultsDataFactory, ++ AddressRepositoryInterface $addressRepository, ++ CustomerMetadataInterface $customerMetadataService, ++ CustomerRegistry $customerRegistry, ++ PsrLogger $logger, ++ Encryptor $encryptor, ++ ConfigShare $configShare, ++ StringHelper $stringHelper, ++ CustomerRepositoryInterface $customerRepository, ++ ScopeConfigInterface $scopeConfig, ++ TransportBuilder $transportBuilder, ++ DataObjectProcessor $dataProcessor, ++ Registry $registry, ++ CustomerViewHelper $customerViewHelper, ++ DateTime $dateTime, ++ CustomerModel $customerModel, ++ ObjectFactory $objectFactory, ++ ExtensibleDataObjectConverter $extensibleDataObjectConverter, ++ AuthorizationInterface $authorization ++ ) { ++ $this->authorization = $authorization; ++ parent::__construct( ++ $customerFactory, ++ $eventManager, ++ $storeManager, ++ $mathRandom, ++ $validator, ++ $validationResultsDataFactory, ++ $addressRepository, ++ $customerMetadataService, ++ $customerRegistry, ++ $logger, ++ $encryptor, ++ $configShare, ++ $stringHelper, ++ $customerRepository, ++ $scopeConfig, ++ $transportBuilder, ++ $dataProcessor, ++ $registry, ++ $customerViewHelper, ++ $dateTime, ++ $customerModel, ++ $objectFactory, ++ $extensibleDataObjectConverter ++ ); ++ } ++ + /** + * @inheritDoc + * +@@ -23,9 +134,30 @@ class AccountManagementApi extends AccountManagement + */ + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + { ++ $this->validateCustomerRequest($customer); + $customer = parent::createAccount($customer, $password, $redirectUrl); + $customer->setConfirmation(null); + + return $customer; + } ++ ++ /** ++ * Validate anonymous request ++ * ++ * @param CustomerInterface $customer ++ * @return void ++ * @throws AuthorizationException ++ */ ++ private function validateCustomerRequest(CustomerInterface $customer): void ++ { ++ $groupId = $customer->getGroupId(); ++ if (isset($groupId) && ++ !$this->authorization->isAllowed(self::ADMIN_RESOURCE) ++ ) { ++ $params = ['resources' => self::ADMIN_RESOURCE]; ++ throw new AuthorizationException( ++ __("The consumer isn't authorized to access %resources.", $params) ++ ); ++ } ++ } + } +diff --git a/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php b/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php +new file mode 100644 +index 000000000000..295b33d2db14 +--- /dev/null ++++ b/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php +@@ -0,0 +1,89 @@ ++authorization = $authorization; ++ } ++ ++ /** ++ * Validate groupId for anonymous request ++ * ++ * @param MassSchedule $massSchedule ++ * @param string $topic ++ * @param array $entitiesArray ++ * @param string|null $groupId ++ * @param string|null $userId ++ * @return null ++ * @throws AuthorizationException ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforePublishMass( ++ MassSchedule $massSchedule, ++ string $topic, ++ array $entitiesArray, ++ string $groupId = null, ++ string $userId = null ++ ) { ++ // only apply the plugin on account create. ++ if ($topic !== self::TOPIC_NAME) { ++ return; ++ } ++ ++ foreach ($entitiesArray as $entityParams) { ++ foreach ($entityParams as $entity) { ++ if ($entity instanceof CustomerInterface) { ++ $groupId = $entity->getGroupId(); ++ if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { ++ $params = ['resources' => self::ADMIN_RESOURCE]; ++ throw new AuthorizationException( ++ __("The consumer isn't authorized to access %resources.", $params) ++ ); ++ } ++ } ++ } ++ } ++ return null; ++ } ++} +diff --git a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php +new file mode 100644 +index 000000000000..074d40021a18 +--- /dev/null ++++ b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php +@@ -0,0 +1,421 @@ ++customerFactory = $this->createPartialMock(CustomerFactory::class, ['create']); ++ $this->manager = $this->getMockForAbstractClass(ManagerInterface::class); ++ $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); ++ $this->random = $this->createMock(Random::class); ++ $this->validator = $this->createMock(Validator::class); ++ $this->validationResultsInterfaceFactory = $this->createMock( ++ ValidationResultsInterfaceFactory::class ++ ); ++ $this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class); ++ $this->customerMetadata = $this->getMockForAbstractClass(CustomerMetadataInterface::class); ++ $this->customerRegistry = $this->createMock(CustomerRegistry::class); ++ ++ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); ++ $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class); ++ $this->share = $this->createMock(Share::class); ++ $this->string = $this->createMock(StringUtils::class); ++ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); ++ $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->transportBuilder = $this->createMock(TransportBuilder::class); ++ $this->dataObjectProcessor = $this->createMock(DataObjectProcessor::class); ++ $this->registry = $this->createMock(Registry::class); ++ $this->customerViewHelper = $this->createMock(View::class); ++ $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); ++ $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class); ++ $this->objectFactory = $this->createMock(DataObjectFactory::class); ++ $this->addressRegistryMock = $this->createMock(AddressRegistry::class); ++ $this->extensibleDataObjectConverter = $this->createMock( ++ ExtensibleDataObjectConverter::class ++ ); ++ $this->allowedCountriesReader = $this->createMock(AllowedCountries::class); ++ $this->customerSecure = $this->getMockBuilder(CustomerSecure::class) ++ ->onlyMethods(['addData', 'setData']) ++ ->addMethods(['setRpToken', 'setRpTokenCreatedAt']) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $this->dateTimeFactory = $this->createMock(DateTimeFactory::class); ++ $this->accountConfirmation = $this->createMock(AccountConfirmation::class); ++ $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); ++ ++ $this->visitorCollectionFactory = $this->getMockBuilder(CollectionFactory::class) ++ ->disableOriginalConstructor() ++ ->onlyMethods(['create']) ++ ->getMock(); ++ $this->sessionManager = $this->getMockBuilder(SessionManagerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->saveHandler = $this->getMockBuilder(SaveHandlerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->authorizationMock = $this->createMock(Authorization::class); ++ $this->objectManagerHelper = new ObjectManagerHelper($this); ++ $this->accountManagement = $this->objectManagerHelper->getObject( ++ AccountManagementApi::class, ++ [ ++ 'customerFactory' => $this->customerFactory, ++ 'eventManager' => $this->manager, ++ 'storeManager' => $this->storeManager, ++ 'mathRandom' => $this->random, ++ 'validator' => $this->validator, ++ 'validationResultsDataFactory' => $this->validationResultsInterfaceFactory, ++ 'addressRepository' => $this->addressRepository, ++ 'customerMetadataService' => $this->customerMetadata, ++ 'customerRegistry' => $this->customerRegistry, ++ 'logger' => $this->logger, ++ 'encryptor' => $this->encryptor, ++ 'configShare' => $this->share, ++ 'stringHelper' => $this->string, ++ 'customerRepository' => $this->customerRepository, ++ 'scopeConfig' => $this->scopeConfig, ++ 'transportBuilder' => $this->transportBuilder, ++ 'dataProcessor' => $this->dataObjectProcessor, ++ 'registry' => $this->registry, ++ 'customerViewHelper' => $this->customerViewHelper, ++ 'dateTime' => $this->dateTime, ++ 'customerModel' => $this->customer, ++ 'objectFactory' => $this->objectFactory, ++ 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, ++ 'dateTimeFactory' => $this->dateTimeFactory, ++ 'accountConfirmation' => $this->accountConfirmation, ++ 'sessionManager' => $this->sessionManager, ++ 'saveHandler' => $this->saveHandler, ++ 'visitorCollectionFactory' => $this->visitorCollectionFactory, ++ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, ++ 'addressRegistry' => $this->addressRegistryMock, ++ 'allowedCountriesReader' => $this->allowedCountriesReader, ++ 'authorization' => $this->authorizationMock ++ ] ++ ); ++ $this->accountManagementMock = $this->createMock(AccountManagement::class); ++ ++ $this->storeMock = $this->getMockBuilder( ++ StoreInterface::class ++ )->disableOriginalConstructor() ++ ->getMock(); ++ } ++ ++ /** ++ * Verify that only authorized request will be able to change groupId ++ * ++ * @param int $groupId ++ * @param int $customerId ++ * @param bool $isAllowed ++ * @param int $willThrowException ++ * @return void ++ * @throws AuthorizationException ++ * @throws LocalizedException ++ * @dataProvider customerDataProvider ++ */ ++ public function testBeforeCreateAccount( ++ int $groupId, ++ int $customerId, ++ bool $isAllowed, ++ int $willThrowException ++ ): void { ++ if ($willThrowException) { ++ $this->expectException(AuthorizationException::class); ++ } else { ++ $this->expectNotToPerformAssertions(); ++ } ++ $this->authorizationMock ++ ->expects($this->once()) ++ ->method('isAllowed') ++ ->with('Magento_Customer::manage') ++ ->willReturn($isAllowed); ++ ++ $customer = $this->getMockBuilder(CustomerInterface::class) ++ ->addMethods(['setData']) ++ ->getMockForAbstractClass(); ++ $customer->method('getGroupId')->willReturn($groupId); ++ $customer->method('getId')->willReturn($customerId); ++ $customer->method('getWebsiteId')->willReturn(2); ++ $customer->method('getStoreId')->willReturn(1); ++ $customer->method('setData')->willReturn(1); ++ ++ $this->customerRepository->method('get')->willReturn($customer); ++ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); ++ $this->customerRepository->method('save')->willReturn($customer); ++ ++ if (!$willThrowException) { ++ $this->accountManagementMock->method('createAccountWithPasswordHash')->willReturn($customer); ++ $this->storeMock->expects($this->any())->method('getId')->willReturnOnConsecutiveCalls(2, 1); ++ $this->random->method('getUniqueHash')->willReturn('testabc'); ++ $date = $this->getMockBuilder(\DateTime::class) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $this->dateTimeFactory->expects(static::once()) ++ ->method('create') ++ ->willReturn($date); ++ $date->expects(static::once()) ++ ->method('format') ++ ->with('Y-m-d H:i:s') ++ ->willReturn('2015-01-01 00:00:00'); ++ $this->customerRegistry->method('retrieveSecureData')->willReturn($this->customerSecure); ++ $this->storeManager->method('getStores') ++ ->willReturn([$this->storeMock]); ++ } ++ $this->accountManagement->createAccount($customer); ++ } ++ ++ /** ++ * @return array ++ */ ++ public function customerDataProvider(): array ++ { ++ return [ ++ [3, 1, false, 1], ++ [3, 1, true, 0] ++ ]; ++ } ++} +diff --git a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php +index 8ff6a8585212..cbe0a18e4b17 100644 +--- a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php ++++ b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php +@@ -1222,7 +1222,6 @@ public function testCreateAccountWithGroupId(): void + $minPasswordLength = 5; + $minCharacterSetsNum = 2; + $defaultGroupId = 1; +- $requestedGroupId = 3; + + $datetime = $this->prepareDateTimeFactory(); + +@@ -1299,9 +1298,6 @@ public function testCreateAccountWithGroupId(): void + return null; + } + })); +- $customer->expects($this->atLeastOnce()) +- ->method('getGroupId') +- ->willReturn($requestedGroupId); + $customer + ->method('setGroupId') + ->willReturnOnConsecutiveCalls(null, $defaultGroupId); +diff --git a/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php b/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php +new file mode 100644 +index 000000000000..107df2c2863e +--- /dev/null ++++ b/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php +@@ -0,0 +1,112 @@ ++authorizationMock = $this->createMock(Authorization::class); ++ $this->plugin = $objectManager->getObject(AsyncRequestCustomerGroupAuthorization::class, [ ++ 'authorization' => $this->authorizationMock ++ ]); ++ $this->massScheduleMock = $this->createMock(MassSchedule::class); ++ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); ++ } ++ ++ /** ++ * Verify that only authorized request will be able to change groupId ++ * ++ * @param int $groupId ++ * @param int $customerId ++ * @param bool $isAllowed ++ * @param int $willThrowException ++ * @return void ++ * @throws AuthorizationException ++ * @dataProvider customerDataProvider ++ */ ++ public function testBeforePublishMass( ++ int $groupId, ++ int $customerId, ++ bool $isAllowed, ++ int $willThrowException ++ ): void { ++ if ($willThrowException) { ++ $this->expectException(AuthorizationException::class); ++ } else { ++ $this->expectNotToPerformAssertions(); ++ } ++ $customer = $this->getMockForAbstractClass(CustomerInterface::class); ++ $customer->method('getGroupId')->willReturn($groupId); ++ $customer->method('getId')->willReturn($customerId); ++ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); ++ $entitiesArray = [ ++ [$customer, 'Password1', ''] ++ ]; ++ $this->authorizationMock ++ ->expects($this->once()) ++ ->method('isAllowed') ++ ->with('Magento_Customer::manage') ++ ->willReturn($isAllowed); ++ $this->plugin->beforePublishMass( ++ $this->massScheduleMock, ++ 'async.magento.customer.api.accountmanagementinterface.createaccount.post', ++ $entitiesArray, ++ '', ++ '' ++ ); ++ } ++ ++ /** ++ * @return array ++ */ ++ public function customerDataProvider(): array ++ { ++ return [ ++ [3, 1, false, 1], ++ [3, 1, true, 0] ++ ]; ++ } ++} +diff --git a/vendor/magento/module-customer/composer.json b/vendor/magento/module-customer/composer.json +index 2d76da56bff7..ff34d423c2da 100644 +--- a/vendor/magento/module-customer/composer.json ++++ b/vendor/magento/module-customer/composer.json +@@ -35,5 +35,6 @@ + "suggest": { + "magento/module-cookie": "100.4.*", + "magento/module-customer-sample-data": "Sample Data version: 100.4.*", +- "magento/module-webapi": "100.4.*" ++ "magento/module-webapi": "100.4.*", ++ "magento/module-asynchronous-operations": "100.4.*" + }, +diff --git a/vendor/magento/module-customer/etc/di.xml b/vendor/magento/module-customer/etc/di.xml +index 31b79935ad9a..4cda16e121c9 100644 +--- a/vendor/magento/module-customer/etc/di.xml ++++ b/vendor/magento/module-customer/etc/di.xml +@@ -567,4 +567,9 @@ + + + ++ ++ ++ + +diff --git a/vendor/magento/module-quote/etc/webapi.xml b/vendor/magento/module-quote/etc/webapi.xml +index 79d98968ea19..a7cce5b03a26 100644 +--- a/vendor/magento/module-quote/etc/webapi.xml ++++ b/vendor/magento/module-quote/etc/webapi.xml +@@ -98,6 +98,9 @@ + + + ++ ++ %customer_id% ++ + + + +diff --git a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +index 6718087888bc..93555559ac9a 100644 +--- a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php ++++ b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +@@ -8,10 +8,12 @@ + + namespace Magento\WebapiAsync\Controller\Rest\Asynchronous; + ++use Magento\Framework\Api\SimpleDataObjectConverter; + use Magento\Framework\App\ObjectManager; + use Magento\Framework\Exception\AuthorizationException; + use Magento\Framework\Exception\InputException; + use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Reflection\MethodsMap; + use Magento\Framework\Webapi\Exception; + use Magento\Framework\Webapi\Rest\Request as RestRequest; + use Magento\Framework\Webapi\ServiceInputProcessor; +@@ -24,6 +26,8 @@ + + /** + * This class is responsible for retrieving resolved input data ++ * ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + class InputParamsResolver + { +@@ -61,6 +65,11 @@ class InputParamsResolver + */ + private $inputArraySizeLimitValue; + ++ /** ++ * @var MethodsMap ++ */ ++ private $methodsMap; ++ + /** + * Initialize dependencies. + * +@@ -72,6 +81,7 @@ class InputParamsResolver + * @param WebapiInputParamsResolver $inputParamsResolver + * @param bool $isBulk + * @param InputArraySizeLimitValue|null $inputArraySizeLimitValue ++ * @param MethodsMap|null $methodsMap + */ + public function __construct( + RestRequest $request, +@@ -81,7 +91,8 @@ public function __construct( + RequestValidator $requestValidator, + WebapiInputParamsResolver $inputParamsResolver, + bool $isBulk = false, +- ?InputArraySizeLimitValue $inputArraySizeLimitValue = null ++ ?InputArraySizeLimitValue $inputArraySizeLimitValue = null, ++ ?MethodsMap $methodsMap = null + ) { + $this->request = $request; + $this->paramsOverrider = $paramsOverrider; +@@ -92,6 +103,8 @@ public function __construct( + $this->isBulk = $isBulk; + $this->inputArraySizeLimitValue = $inputArraySizeLimitValue ?? ObjectManager::getInstance() + ->get(InputArraySizeLimitValue::class); ++ $this->methodsMap = $methodsMap ?? ObjectManager::getInstance() ++ ->get(MethodsMap::class); + } + + /** +@@ -119,7 +132,13 @@ public function resolve() + $routeServiceMethod = $route->getServiceMethod(); + $this->inputArraySizeLimitValue->set($route->getInputArraySizeLimit()); + ++ $this->validateParameters($routeServiceClass, $routeServiceMethod, array_keys($route->getParameters())); ++ + foreach ($inputData as $key => $singleEntityParams) { ++ if (!is_array($singleEntityParams)) { ++ continue; ++ } ++ + $webapiResolvedParams[$key] = $this->resolveBulkItemParams( + $singleEntityParams, + $routeServiceClass, +@@ -143,11 +162,22 @@ public function getInputData() + $inputData = $this->request->getRequestData(); + + $httpMethod = $this->request->getHttpMethod(); +- if ($httpMethod == RestRequest::HTTP_METHOD_DELETE) { ++ if ($httpMethod === RestRequest::HTTP_METHOD_DELETE) { + $requestBodyParams = $this->request->getBodyParams(); + $inputData = array_merge($requestBodyParams, $inputData); + } +- return $inputData; ++ ++ return array_map(function ($singleEntityParams) { ++ if (is_array($singleEntityParams)) { ++ $singleEntityParams = $this->filterInputData($singleEntityParams); ++ $singleEntityParams = $this->paramsOverrider->override( ++ $singleEntityParams, ++ $this->getRoute()->getParameters() ++ ); ++ } ++ ++ return $singleEntityParams; ++ }, $inputData); + } + + /** +@@ -180,4 +210,64 @@ private function resolveBulkItemParams(array $inputData, string $serviceClass, s + { + return $this->serviceInputProcessor->process($serviceClass, $serviceMethod, $inputData); + } ++ ++ /** ++ * Validates InputData ++ * ++ * @param array $inputData ++ * @return array ++ */ ++ private function filterInputData(array $inputData): array ++ { ++ $result = []; ++ ++ $data = array_filter($inputData, function ($k) use (&$result) { ++ $key = is_string($k) ? strtolower(str_replace('_', "", $k)) : $k; ++ return !isset($result[$key]) && ($result[$key] = true); ++ }, ARRAY_FILTER_USE_KEY); ++ ++ return array_map(function ($value) { ++ return is_array($value) ? $this->filterInputData($value) : $value; ++ }, $data); ++ } ++ ++ /** ++ * Validate that parameters are really used in the current request. ++ * ++ * @param string $serviceClassName ++ * @param string $serviceMethodName ++ * @param array $paramOverriders ++ */ ++ private function validateParameters( ++ string $serviceClassName, ++ string $serviceMethodName, ++ array $paramOverriders ++ ): void { ++ $methodParams = $this->methodsMap->getMethodParams($serviceClassName, $serviceMethodName); ++ foreach ($paramOverriders as $key => $param) { ++ $arrayKeys = explode('.', $param ?? ''); ++ $value = array_shift($arrayKeys); ++ ++ foreach ($methodParams as $serviceMethodParam) { ++ $serviceMethodParamName = $serviceMethodParam[MethodsMap::METHOD_META_NAME]; ++ $serviceMethodType = $serviceMethodParam[MethodsMap::METHOD_META_TYPE]; ++ ++ $camelCaseValue = SimpleDataObjectConverter::snakeCaseToCamelCase($value); ++ if ($serviceMethodParamName === $value || $serviceMethodParamName === $camelCaseValue) { ++ if (count($arrayKeys) > 0) { ++ $camelCaseKey = SimpleDataObjectConverter::snakeCaseToCamelCase('set_' . $arrayKeys[0]); ++ $this->validateParameters($serviceMethodType, $camelCaseKey, [implode('.', $arrayKeys)]); ++ } ++ unset($paramOverriders[$key]); ++ break; ++ } ++ } ++ } ++ ++ if (!empty($paramOverriders)) { ++ $message = 'The current request does not expect the next parameters: ' ++ . implode(', ', $paramOverriders); ++ throw new \UnexpectedValueException(__($message)->__toString()); ++ } ++ } + } +diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +index ce9e4ee94178..e08fe0388cfb 100644 +--- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php ++++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +@@ -10,10 +10,13 @@ + + class GuestCartManagementTest extends WebapiAbstract + { +- const SERVICE_VERSION = 'V1'; +- const SERVICE_NAME = 'quoteGuestCartManagementV1'; +- const RESOURCE_PATH = '/V1/guest-carts/'; ++ public const SERVICE_VERSION = 'V1'; ++ public const SERVICE_NAME = 'quoteGuestCartManagementV1'; ++ public const RESOURCE_PATH = '/V1/guest-carts/'; + ++ /** ++ * @var array List of created quotes ++ */ + protected $createdQuotes = []; + + /** +@@ -339,7 +342,7 @@ public function testPlaceOrder() + public function testAssignCustomerByGuestUser() + { + $this->expectException(\Exception::class); +- $this->expectExceptionMessage('You don\'t have the correct permissions to assign the customer to the cart.'); ++ $this->expectExceptionMessage('Enter and try again.'); + + /** @var $quote \Magento\Quote\Model\Quote */ + $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); +diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +index 9991dd4e05fe..dc98670bd000 100644 +--- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt ++++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +@@ -110,4 +110,5 @@ app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php + app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php + app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml + app/code/Magento/GoogleGtag +-app/code/Magento/AdminAdobeIms/Observer/AuthObserver +\ No newline at end of file ++app/code/Magento/AdminAdobeIms/Observer/AuthObserver ++app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php diff --git a/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.6.patch b/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.6.patch new file mode 100644 index 00000000..d18b1e8b --- /dev/null +++ b/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.6.patch @@ -0,0 +1,1090 @@ +diff --git a/vendor/magento/module-customer/Model/AccountManagement.php b/vendor/magento/module-customer/Model/AccountManagement.php +index 702b6aeb6865..bfebafeb0330 100644 +--- a/vendor/magento/module-customer/Model/AccountManagement.php ++++ b/vendor/magento/module-customer/Model/AccountManagement.php +@@ -882,11 +882,6 @@ public function getConfirmationStatus($customerId) + */ + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + { +- $groupId = $customer->getGroupId(); +- if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { +- $customer->setGroupId(null); +- } +- + if ($password !== null) { + $this->checkPasswordStrength($password); + $customerEmail = $customer->getEmail(); +diff --git a/vendor/magento/module-customer/Model/AccountManagementApi.php b/vendor/magento/module-customer/Model/AccountManagementApi.php +index 02a05705b57e..8b4f78ab26c7 100644 +--- a/vendor/magento/module-customer/Model/AccountManagementApi.php ++++ b/vendor/magento/module-customer/Model/AccountManagementApi.php +@@ -6,16 +6,127 @@ + + namespace Magento\Customer\Model; + ++use Magento\Customer\Api\AddressRepositoryInterface; ++use Magento\Customer\Api\CustomerMetadataInterface; ++use Magento\Customer\Api\CustomerRepositoryInterface; + use Magento\Customer\Api\Data\CustomerInterface; ++use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; ++use Magento\Customer\Helper\View as CustomerViewHelper; ++use Magento\Customer\Model\Config\Share as ConfigShare; ++use Magento\Customer\Model\Customer as CustomerModel; ++use Magento\Customer\Model\Metadata\Validator; ++use Magento\Framework\Api\ExtensibleDataObjectConverter; ++use Magento\Framework\App\Config\ScopeConfigInterface; ++use Magento\Framework\AuthorizationInterface; ++use Magento\Framework\DataObjectFactory as ObjectFactory; ++use Magento\Framework\Encryption\EncryptorInterface as Encryptor; ++use Magento\Framework\Event\ManagerInterface; ++use Magento\Framework\Exception\AuthorizationException; ++use Magento\Framework\Mail\Template\TransportBuilder; ++use Magento\Framework\Math\Random; ++use Magento\Framework\Reflection\DataObjectProcessor; ++use Magento\Framework\Registry; ++use Magento\Framework\Stdlib\DateTime; ++use Magento\Framework\Stdlib\StringUtils as StringHelper; ++use Magento\Store\Model\StoreManagerInterface; ++use Psr\Log\LoggerInterface as PsrLogger; + + /** + * Account Management service implementation for external API access. ++ * + * Handle various customer account actions. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + class AccountManagementApi extends AccountManagement + { ++ /** ++ * @var AuthorizationInterface ++ */ ++ private $authorization; ++ ++ /** ++ * @param CustomerFactory $customerFactory ++ * @param ManagerInterface $eventManager ++ * @param StoreManagerInterface $storeManager ++ * @param Random $mathRandom ++ * @param Validator $validator ++ * @param ValidationResultsInterfaceFactory $validationResultsDataFactory ++ * @param AddressRepositoryInterface $addressRepository ++ * @param CustomerMetadataInterface $customerMetadataService ++ * @param CustomerRegistry $customerRegistry ++ * @param PsrLogger $logger ++ * @param Encryptor $encryptor ++ * @param ConfigShare $configShare ++ * @param StringHelper $stringHelper ++ * @param CustomerRepositoryInterface $customerRepository ++ * @param ScopeConfigInterface $scopeConfig ++ * @param TransportBuilder $transportBuilder ++ * @param DataObjectProcessor $dataProcessor ++ * @param Registry $registry ++ * @param CustomerViewHelper $customerViewHelper ++ * @param DateTime $dateTime ++ * @param CustomerModel $customerModel ++ * @param ObjectFactory $objectFactory ++ * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter ++ * @param AuthorizationInterface $authorization ++ * @SuppressWarnings(PHPMD.ExcessiveParameterList) ++ */ ++ public function __construct( ++ CustomerFactory $customerFactory, ++ ManagerInterface $eventManager, ++ StoreManagerInterface $storeManager, ++ Random $mathRandom, ++ Validator $validator, ++ ValidationResultsInterfaceFactory $validationResultsDataFactory, ++ AddressRepositoryInterface $addressRepository, ++ CustomerMetadataInterface $customerMetadataService, ++ CustomerRegistry $customerRegistry, ++ PsrLogger $logger, ++ Encryptor $encryptor, ++ ConfigShare $configShare, ++ StringHelper $stringHelper, ++ CustomerRepositoryInterface $customerRepository, ++ ScopeConfigInterface $scopeConfig, ++ TransportBuilder $transportBuilder, ++ DataObjectProcessor $dataProcessor, ++ Registry $registry, ++ CustomerViewHelper $customerViewHelper, ++ DateTime $dateTime, ++ CustomerModel $customerModel, ++ ObjectFactory $objectFactory, ++ ExtensibleDataObjectConverter $extensibleDataObjectConverter, ++ AuthorizationInterface $authorization ++ ) { ++ $this->authorization = $authorization; ++ parent::__construct( ++ $customerFactory, ++ $eventManager, ++ $storeManager, ++ $mathRandom, ++ $validator, ++ $validationResultsDataFactory, ++ $addressRepository, ++ $customerMetadataService, ++ $customerRegistry, ++ $logger, ++ $encryptor, ++ $configShare, ++ $stringHelper, ++ $customerRepository, ++ $scopeConfig, ++ $transportBuilder, ++ $dataProcessor, ++ $registry, ++ $customerViewHelper, ++ $dateTime, ++ $customerModel, ++ $objectFactory, ++ $extensibleDataObjectConverter ++ ); ++ } ++ + /** + * @inheritDoc + * +@@ -23,9 +134,30 @@ class AccountManagementApi extends AccountManagement + */ + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + { ++ $this->validateCustomerRequest($customer); + $customer = parent::createAccount($customer, $password, $redirectUrl); + $customer->setConfirmation(null); + + return $customer; + } ++ ++ /** ++ * Validate anonymous request ++ * ++ * @param CustomerInterface $customer ++ * @return void ++ * @throws AuthorizationException ++ */ ++ private function validateCustomerRequest(CustomerInterface $customer): void ++ { ++ $groupId = $customer->getGroupId(); ++ if (isset($groupId) && ++ !$this->authorization->isAllowed(self::ADMIN_RESOURCE) ++ ) { ++ $params = ['resources' => self::ADMIN_RESOURCE]; ++ throw new AuthorizationException( ++ __("The consumer isn't authorized to access %resources.", $params) ++ ); ++ } ++ } + } +diff --git a/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php b/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php +new file mode 100644 +index 000000000000..cdda3016694c +--- /dev/null ++++ b/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php +@@ -0,0 +1,90 @@ ++authorization = $authorization; ++ } ++ ++ /** ++ * Validate groupId for anonymous request ++ * ++ * @param MassSchedule $massSchedule ++ * @param string $topic ++ * @param array $entitiesArray ++ * @param string|null $groupId ++ * @param string|null $userId ++ * @return null ++ * @throws AuthorizationException ++ * @SuppressWarnings(PHPMD.UnusedFormalParameter) ++ */ ++ public function beforePublishMass( ++ MassSchedule $massSchedule, ++ string $topic, ++ array $entitiesArray, ++ string $groupId = null, ++ string $userId = null ++ ) { ++ // only apply the plugin on account create. ++ if ($topic !== self::TOPIC_NAME) { ++ return; ++ } ++ ++ foreach ($entitiesArray as $entityParams) { ++ foreach ($entityParams as $entity) { ++ if ($entity instanceof CustomerInterface) { ++ $groupId = $entity->getGroupId(); ++ if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { ++ $params = ['resources' => self::ADMIN_RESOURCE]; ++ throw new AuthorizationException( ++ __("The consumer isn't authorized to access %resources.", $params) ++ ); ++ } ++ } ++ } ++ } ++ return null; ++ } ++} +diff --git a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php +new file mode 100644 +index 000000000000..074d40021a18 +--- /dev/null ++++ b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementApiTest.php +@@ -0,0 +1,421 @@ ++customerFactory = $this->createPartialMock(CustomerFactory::class, ['create']); ++ $this->manager = $this->getMockForAbstractClass(ManagerInterface::class); ++ $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); ++ $this->random = $this->createMock(Random::class); ++ $this->validator = $this->createMock(Validator::class); ++ $this->validationResultsInterfaceFactory = $this->createMock( ++ ValidationResultsInterfaceFactory::class ++ ); ++ $this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class); ++ $this->customerMetadata = $this->getMockForAbstractClass(CustomerMetadataInterface::class); ++ $this->customerRegistry = $this->createMock(CustomerRegistry::class); ++ ++ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); ++ $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class); ++ $this->share = $this->createMock(Share::class); ++ $this->string = $this->createMock(StringUtils::class); ++ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); ++ $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->transportBuilder = $this->createMock(TransportBuilder::class); ++ $this->dataObjectProcessor = $this->createMock(DataObjectProcessor::class); ++ $this->registry = $this->createMock(Registry::class); ++ $this->customerViewHelper = $this->createMock(View::class); ++ $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); ++ $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class); ++ $this->objectFactory = $this->createMock(DataObjectFactory::class); ++ $this->addressRegistryMock = $this->createMock(AddressRegistry::class); ++ $this->extensibleDataObjectConverter = $this->createMock( ++ ExtensibleDataObjectConverter::class ++ ); ++ $this->allowedCountriesReader = $this->createMock(AllowedCountries::class); ++ $this->customerSecure = $this->getMockBuilder(CustomerSecure::class) ++ ->onlyMethods(['addData', 'setData']) ++ ->addMethods(['setRpToken', 'setRpTokenCreatedAt']) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $this->dateTimeFactory = $this->createMock(DateTimeFactory::class); ++ $this->accountConfirmation = $this->createMock(AccountConfirmation::class); ++ $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); ++ ++ $this->visitorCollectionFactory = $this->getMockBuilder(CollectionFactory::class) ++ ->disableOriginalConstructor() ++ ->onlyMethods(['create']) ++ ->getMock(); ++ $this->sessionManager = $this->getMockBuilder(SessionManagerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->saveHandler = $this->getMockBuilder(SaveHandlerInterface::class) ++ ->disableOriginalConstructor() ++ ->getMockForAbstractClass(); ++ $this->authorizationMock = $this->createMock(Authorization::class); ++ $this->objectManagerHelper = new ObjectManagerHelper($this); ++ $this->accountManagement = $this->objectManagerHelper->getObject( ++ AccountManagementApi::class, ++ [ ++ 'customerFactory' => $this->customerFactory, ++ 'eventManager' => $this->manager, ++ 'storeManager' => $this->storeManager, ++ 'mathRandom' => $this->random, ++ 'validator' => $this->validator, ++ 'validationResultsDataFactory' => $this->validationResultsInterfaceFactory, ++ 'addressRepository' => $this->addressRepository, ++ 'customerMetadataService' => $this->customerMetadata, ++ 'customerRegistry' => $this->customerRegistry, ++ 'logger' => $this->logger, ++ 'encryptor' => $this->encryptor, ++ 'configShare' => $this->share, ++ 'stringHelper' => $this->string, ++ 'customerRepository' => $this->customerRepository, ++ 'scopeConfig' => $this->scopeConfig, ++ 'transportBuilder' => $this->transportBuilder, ++ 'dataProcessor' => $this->dataObjectProcessor, ++ 'registry' => $this->registry, ++ 'customerViewHelper' => $this->customerViewHelper, ++ 'dateTime' => $this->dateTime, ++ 'customerModel' => $this->customer, ++ 'objectFactory' => $this->objectFactory, ++ 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, ++ 'dateTimeFactory' => $this->dateTimeFactory, ++ 'accountConfirmation' => $this->accountConfirmation, ++ 'sessionManager' => $this->sessionManager, ++ 'saveHandler' => $this->saveHandler, ++ 'visitorCollectionFactory' => $this->visitorCollectionFactory, ++ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, ++ 'addressRegistry' => $this->addressRegistryMock, ++ 'allowedCountriesReader' => $this->allowedCountriesReader, ++ 'authorization' => $this->authorizationMock ++ ] ++ ); ++ $this->accountManagementMock = $this->createMock(AccountManagement::class); ++ ++ $this->storeMock = $this->getMockBuilder( ++ StoreInterface::class ++ )->disableOriginalConstructor() ++ ->getMock(); ++ } ++ ++ /** ++ * Verify that only authorized request will be able to change groupId ++ * ++ * @param int $groupId ++ * @param int $customerId ++ * @param bool $isAllowed ++ * @param int $willThrowException ++ * @return void ++ * @throws AuthorizationException ++ * @throws LocalizedException ++ * @dataProvider customerDataProvider ++ */ ++ public function testBeforeCreateAccount( ++ int $groupId, ++ int $customerId, ++ bool $isAllowed, ++ int $willThrowException ++ ): void { ++ if ($willThrowException) { ++ $this->expectException(AuthorizationException::class); ++ } else { ++ $this->expectNotToPerformAssertions(); ++ } ++ $this->authorizationMock ++ ->expects($this->once()) ++ ->method('isAllowed') ++ ->with('Magento_Customer::manage') ++ ->willReturn($isAllowed); ++ ++ $customer = $this->getMockBuilder(CustomerInterface::class) ++ ->addMethods(['setData']) ++ ->getMockForAbstractClass(); ++ $customer->method('getGroupId')->willReturn($groupId); ++ $customer->method('getId')->willReturn($customerId); ++ $customer->method('getWebsiteId')->willReturn(2); ++ $customer->method('getStoreId')->willReturn(1); ++ $customer->method('setData')->willReturn(1); ++ ++ $this->customerRepository->method('get')->willReturn($customer); ++ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); ++ $this->customerRepository->method('save')->willReturn($customer); ++ ++ if (!$willThrowException) { ++ $this->accountManagementMock->method('createAccountWithPasswordHash')->willReturn($customer); ++ $this->storeMock->expects($this->any())->method('getId')->willReturnOnConsecutiveCalls(2, 1); ++ $this->random->method('getUniqueHash')->willReturn('testabc'); ++ $date = $this->getMockBuilder(\DateTime::class) ++ ->disableOriginalConstructor() ++ ->getMock(); ++ $this->dateTimeFactory->expects(static::once()) ++ ->method('create') ++ ->willReturn($date); ++ $date->expects(static::once()) ++ ->method('format') ++ ->with('Y-m-d H:i:s') ++ ->willReturn('2015-01-01 00:00:00'); ++ $this->customerRegistry->method('retrieveSecureData')->willReturn($this->customerSecure); ++ $this->storeManager->method('getStores') ++ ->willReturn([$this->storeMock]); ++ } ++ $this->accountManagement->createAccount($customer); ++ } ++ ++ /** ++ * @return array ++ */ ++ public function customerDataProvider(): array ++ { ++ return [ ++ [3, 1, false, 1], ++ [3, 1, true, 0] ++ ]; ++ } ++} +diff --git a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php +index 9e68d53fd594..e2b507f6fe37 100644 +--- a/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php ++++ b/vendor/magento/module-customer/Test/Unit/Model/AccountManagementTest.php +@@ -1222,7 +1222,6 @@ public function testCreateAccountWithGroupId(): void + $minPasswordLength = 5; + $minCharacterSetsNum = 2; + $defaultGroupId = 1; +- $requestedGroupId = 3; + + $datetime = $this->prepareDateTimeFactory(); + +@@ -1299,9 +1298,6 @@ public function testCreateAccountWithGroupId(): void + return null; + } + })); +- $customer->expects($this->atLeastOnce()) +- ->method('getGroupId') +- ->willReturn($requestedGroupId); + $customer + ->method('setGroupId') + ->willReturnOnConsecutiveCalls(null, $defaultGroupId); +diff --git a/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php b/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php +new file mode 100644 +index 000000000000..107df2c2863e +--- /dev/null ++++ b/vendor/magento/module-customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php +@@ -0,0 +1,112 @@ ++authorizationMock = $this->createMock(Authorization::class); ++ $this->plugin = $objectManager->getObject(AsyncRequestCustomerGroupAuthorization::class, [ ++ 'authorization' => $this->authorizationMock ++ ]); ++ $this->massScheduleMock = $this->createMock(MassSchedule::class); ++ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); ++ } ++ ++ /** ++ * Verify that only authorized request will be able to change groupId ++ * ++ * @param int $groupId ++ * @param int $customerId ++ * @param bool $isAllowed ++ * @param int $willThrowException ++ * @return void ++ * @throws AuthorizationException ++ * @dataProvider customerDataProvider ++ */ ++ public function testBeforePublishMass( ++ int $groupId, ++ int $customerId, ++ bool $isAllowed, ++ int $willThrowException ++ ): void { ++ if ($willThrowException) { ++ $this->expectException(AuthorizationException::class); ++ } else { ++ $this->expectNotToPerformAssertions(); ++ } ++ $customer = $this->getMockForAbstractClass(CustomerInterface::class); ++ $customer->method('getGroupId')->willReturn($groupId); ++ $customer->method('getId')->willReturn($customerId); ++ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); ++ $entitiesArray = [ ++ [$customer, 'Password1', ''] ++ ]; ++ $this->authorizationMock ++ ->expects($this->once()) ++ ->method('isAllowed') ++ ->with('Magento_Customer::manage') ++ ->willReturn($isAllowed); ++ $this->plugin->beforePublishMass( ++ $this->massScheduleMock, ++ 'async.magento.customer.api.accountmanagementinterface.createaccount.post', ++ $entitiesArray, ++ '', ++ '' ++ ); ++ } ++ ++ /** ++ * @return array ++ */ ++ public function customerDataProvider(): array ++ { ++ return [ ++ [3, 1, false, 1], ++ [3, 1, true, 0] ++ ]; ++ } ++} +diff --git a/vendor/magento/module-customer/composer.json b/vendor/magento/module-customer/composer.json +index ef2047644759..39c82c20f2ec 100644 +--- a/vendor/magento/module-customer/composer.json ++++ b/vendor/magento/module-customer/composer.json +@@ -29,5 +29,6 @@ + "suggest": { + "magento/module-cookie": "100.4.*", + "magento/module-customer-sample-data": "Sample Data version: 100.4.*", +- "magento/module-webapi": "100.4.*" ++ "magento/module-webapi": "100.4.*", ++ "magento/module-asynchronous-operations": "100.4.*" + }, +diff --git a/vendor/magento/module-customer/etc/di.xml b/vendor/magento/module-customer/etc/di.xml +index b178f51f8919..96fd4b86be70 100644 +--- a/vendor/magento/module-customer/etc/di.xml ++++ b/vendor/magento/module-customer/etc/di.xml +@@ -585,4 +585,9 @@ + + + ++ ++ ++ + +diff --git a/vendor/magento/module-quote/etc/webapi.xml b/vendor/magento/module-quote/etc/webapi.xml +index 79d98968ea19..a7cce5b03a26 100644 +--- a/vendor/magento/module-quote/etc/webapi.xml ++++ b/vendor/magento/module-quote/etc/webapi.xml +@@ -98,6 +98,9 @@ + + + ++ ++ %customer_id% ++ + + + +diff --git a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +index 6718087888bc..93555559ac9a 100644 +--- a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php ++++ b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +@@ -8,10 +8,12 @@ + + namespace Magento\WebapiAsync\Controller\Rest\Asynchronous; + ++use Magento\Framework\Api\SimpleDataObjectConverter; + use Magento\Framework\App\ObjectManager; + use Magento\Framework\Exception\AuthorizationException; + use Magento\Framework\Exception\InputException; + use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Reflection\MethodsMap; + use Magento\Framework\Webapi\Exception; + use Magento\Framework\Webapi\Rest\Request as RestRequest; + use Magento\Framework\Webapi\ServiceInputProcessor; +@@ -24,6 +26,8 @@ + + /** + * This class is responsible for retrieving resolved input data ++ * ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + class InputParamsResolver + { +@@ -61,6 +65,11 @@ class InputParamsResolver + */ + private $inputArraySizeLimitValue; + ++ /** ++ * @var MethodsMap ++ */ ++ private $methodsMap; ++ + /** + * Initialize dependencies. + * +@@ -72,6 +81,7 @@ class InputParamsResolver + * @param WebapiInputParamsResolver $inputParamsResolver + * @param bool $isBulk + * @param InputArraySizeLimitValue|null $inputArraySizeLimitValue ++ * @param MethodsMap|null $methodsMap + */ + public function __construct( + RestRequest $request, +@@ -81,7 +91,8 @@ public function __construct( + RequestValidator $requestValidator, + WebapiInputParamsResolver $inputParamsResolver, + bool $isBulk = false, +- ?InputArraySizeLimitValue $inputArraySizeLimitValue = null ++ ?InputArraySizeLimitValue $inputArraySizeLimitValue = null, ++ ?MethodsMap $methodsMap = null + ) { + $this->request = $request; + $this->paramsOverrider = $paramsOverrider; +@@ -92,6 +103,8 @@ public function __construct( + $this->isBulk = $isBulk; + $this->inputArraySizeLimitValue = $inputArraySizeLimitValue ?? ObjectManager::getInstance() + ->get(InputArraySizeLimitValue::class); ++ $this->methodsMap = $methodsMap ?? ObjectManager::getInstance() ++ ->get(MethodsMap::class); + } + + /** +@@ -119,7 +132,13 @@ public function resolve() + $routeServiceMethod = $route->getServiceMethod(); + $this->inputArraySizeLimitValue->set($route->getInputArraySizeLimit()); + ++ $this->validateParameters($routeServiceClass, $routeServiceMethod, array_keys($route->getParameters())); ++ + foreach ($inputData as $key => $singleEntityParams) { ++ if (!is_array($singleEntityParams)) { ++ continue; ++ } ++ + $webapiResolvedParams[$key] = $this->resolveBulkItemParams( + $singleEntityParams, + $routeServiceClass, +@@ -143,11 +162,22 @@ public function getInputData() + $inputData = $this->request->getRequestData(); + + $httpMethod = $this->request->getHttpMethod(); +- if ($httpMethod == RestRequest::HTTP_METHOD_DELETE) { ++ if ($httpMethod === RestRequest::HTTP_METHOD_DELETE) { + $requestBodyParams = $this->request->getBodyParams(); + $inputData = array_merge($requestBodyParams, $inputData); + } +- return $inputData; ++ ++ return array_map(function ($singleEntityParams) { ++ if (is_array($singleEntityParams)) { ++ $singleEntityParams = $this->filterInputData($singleEntityParams); ++ $singleEntityParams = $this->paramsOverrider->override( ++ $singleEntityParams, ++ $this->getRoute()->getParameters() ++ ); ++ } ++ ++ return $singleEntityParams; ++ }, $inputData); + } + + /** +@@ -180,4 +210,64 @@ private function resolveBulkItemParams(array $inputData, string $serviceClass, s + { + return $this->serviceInputProcessor->process($serviceClass, $serviceMethod, $inputData); + } ++ ++ /** ++ * Validates InputData ++ * ++ * @param array $inputData ++ * @return array ++ */ ++ private function filterInputData(array $inputData): array ++ { ++ $result = []; ++ ++ $data = array_filter($inputData, function ($k) use (&$result) { ++ $key = is_string($k) ? strtolower(str_replace('_', "", $k)) : $k; ++ return !isset($result[$key]) && ($result[$key] = true); ++ }, ARRAY_FILTER_USE_KEY); ++ ++ return array_map(function ($value) { ++ return is_array($value) ? $this->filterInputData($value) : $value; ++ }, $data); ++ } ++ ++ /** ++ * Validate that parameters are really used in the current request. ++ * ++ * @param string $serviceClassName ++ * @param string $serviceMethodName ++ * @param array $paramOverriders ++ */ ++ private function validateParameters( ++ string $serviceClassName, ++ string $serviceMethodName, ++ array $paramOverriders ++ ): void { ++ $methodParams = $this->methodsMap->getMethodParams($serviceClassName, $serviceMethodName); ++ foreach ($paramOverriders as $key => $param) { ++ $arrayKeys = explode('.', $param ?? ''); ++ $value = array_shift($arrayKeys); ++ ++ foreach ($methodParams as $serviceMethodParam) { ++ $serviceMethodParamName = $serviceMethodParam[MethodsMap::METHOD_META_NAME]; ++ $serviceMethodType = $serviceMethodParam[MethodsMap::METHOD_META_TYPE]; ++ ++ $camelCaseValue = SimpleDataObjectConverter::snakeCaseToCamelCase($value); ++ if ($serviceMethodParamName === $value || $serviceMethodParamName === $camelCaseValue) { ++ if (count($arrayKeys) > 0) { ++ $camelCaseKey = SimpleDataObjectConverter::snakeCaseToCamelCase('set_' . $arrayKeys[0]); ++ $this->validateParameters($serviceMethodType, $camelCaseKey, [implode('.', $arrayKeys)]); ++ } ++ unset($paramOverriders[$key]); ++ break; ++ } ++ } ++ } ++ ++ if (!empty($paramOverriders)) { ++ $message = 'The current request does not expect the next parameters: ' ++ . implode(', ', $paramOverriders); ++ throw new \UnexpectedValueException(__($message)->__toString()); ++ } ++ } + } +diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +index ce9e4ee94178..e08fe0388cfb 100644 +--- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php ++++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +@@ -10,10 +10,13 @@ + + class GuestCartManagementTest extends WebapiAbstract + { +- const SERVICE_VERSION = 'V1'; +- const SERVICE_NAME = 'quoteGuestCartManagementV1'; +- const RESOURCE_PATH = '/V1/guest-carts/'; ++ public const SERVICE_VERSION = 'V1'; ++ public const SERVICE_NAME = 'quoteGuestCartManagementV1'; ++ public const RESOURCE_PATH = '/V1/guest-carts/'; + ++ /** ++ * @var array List of created quotes ++ */ + protected $createdQuotes = []; + + /** +@@ -339,7 +342,7 @@ public function testPlaceOrder() + public function testAssignCustomerByGuestUser() + { + $this->expectException(\Exception::class); +- $this->expectExceptionMessage('You don\'t have the correct permissions to assign the customer to the cart.'); ++ $this->expectExceptionMessage('Enter and try again.'); + + /** @var $quote \Magento\Quote\Model\Quote */ + $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); +diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +index 80fe4ec247a6..50285d7492e8 100644 +--- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt ++++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +@@ -111,3 +111,4 @@ app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/ed + app/code/Magento/GoogleGtag + app/code/Magento/AdminAdobeIms/Observer/AuthObserver + app/code/Magento/OpenSearch/SearchAdapter/Adapter ++app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php diff --git a/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.7.patch b/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.7.patch new file mode 100644 index 00000000..038b6c83 --- /dev/null +++ b/patches/MCLOUD-13240__Patch_for_CVE_2025_24434_improve_web_api_async__2.4.7.patch @@ -0,0 +1,245 @@ +diff --git a/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php b/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php +index 5b5c8ce1fc0c..295b33d2db14 100644 +--- a/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php ++++ b/vendor/magento/module-customer/Plugin/AsyncRequestCustomerGroupAuthorization.php +@@ -9,7 +9,6 @@ + namespace Magento\Customer\Plugin; + + use Magento\Customer\Api\Data\CustomerInterface; +-use Magento\Framework\App\ObjectManager; + use Magento\Framework\AuthorizationInterface; + use Magento\Framework\Exception\AuthorizationException; + use Magento\AsynchronousOperations\Model\MassSchedule; +@@ -26,6 +25,13 @@ class AsyncRequestCustomerGroupAuthorization + */ + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; + ++ /** ++ * account create topic name ++ * ++ * @var string ++ */ ++ private const TOPIC_NAME = 'async.magento.customer.api.accountmanagementinterface.createaccount.post'; ++ + /** + * @var AuthorizationInterface + */ +@@ -60,6 +66,11 @@ public function beforePublishMass( + string $groupId = null, + string $userId = null + ) { ++ // only apply the plugin on account create. ++ if ($topic !== self::TOPIC_NAME) { ++ return; ++ } ++ + foreach ($entitiesArray as $entityParams) { + foreach ($entityParams as $entity) { + if ($entity instanceof CustomerInterface) { +diff --git a/vendor/magento/module-quote/etc/webapi.xml b/vendor/magento/module-quote/etc/webapi.xml +index 79d98968ea19..a7cce5b03a26 100644 +--- a/vendor/magento/module-quote/etc/webapi.xml ++++ b/vendor/magento/module-quote/etc/webapi.xml +@@ -98,6 +98,9 @@ + + + ++ ++ %customer_id% ++ + + + +diff --git a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +index 6718087888bc..6e159eaddf16 100644 +--- a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php ++++ b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +@@ -8,10 +8,12 @@ + + namespace Magento\WebapiAsync\Controller\Rest\Asynchronous; + ++use Magento\Framework\Api\SimpleDataObjectConverter; + use Magento\Framework\App\ObjectManager; + use Magento\Framework\Exception\AuthorizationException; + use Magento\Framework\Exception\InputException; + use Magento\Framework\Exception\LocalizedException; ++use Magento\Framework\Reflection\MethodsMap; + use Magento\Framework\Webapi\Exception; + use Magento\Framework\Webapi\Rest\Request as RestRequest; + use Magento\Framework\Webapi\ServiceInputProcessor; +@@ -24,6 +26,8 @@ + + /** + * This class is responsible for retrieving resolved input data ++ * ++ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + class InputParamsResolver + { +@@ -61,6 +65,11 @@ class InputParamsResolver + */ + private $inputArraySizeLimitValue; + ++ /** ++ * @var MethodsMap ++ */ ++ private $methodsMap; ++ + /** + * Initialize dependencies. + * +@@ -72,6 +81,7 @@ class InputParamsResolver + * @param WebapiInputParamsResolver $inputParamsResolver + * @param bool $isBulk + * @param InputArraySizeLimitValue|null $inputArraySizeLimitValue ++ * @param MethodsMap|null $methodsMap + */ + public function __construct( + RestRequest $request, +@@ -81,7 +91,8 @@ public function __construct( + RequestValidator $requestValidator, + WebapiInputParamsResolver $inputParamsResolver, + bool $isBulk = false, +- ?InputArraySizeLimitValue $inputArraySizeLimitValue = null ++ ?InputArraySizeLimitValue $inputArraySizeLimitValue = null, ++ ?MethodsMap $methodsMap = null + ) { + $this->request = $request; + $this->paramsOverrider = $paramsOverrider; +@@ -92,6 +103,8 @@ public function __construct( + $this->isBulk = $isBulk; + $this->inputArraySizeLimitValue = $inputArraySizeLimitValue ?? ObjectManager::getInstance() + ->get(InputArraySizeLimitValue::class); ++ $this->methodsMap = $methodsMap ?? ObjectManager::getInstance() ++ ->get(MethodsMap::class); + } + + /** +@@ -119,7 +132,13 @@ public function resolve() + $routeServiceMethod = $route->getServiceMethod(); + $this->inputArraySizeLimitValue->set($route->getInputArraySizeLimit()); + ++ $this->validateParameters($routeServiceClass, $routeServiceMethod, array_keys($route->getParameters())); ++ + foreach ($inputData as $key => $singleEntityParams) { ++ if (!is_array($singleEntityParams)) { ++ continue; ++ } ++ + $webapiResolvedParams[$key] = $this->resolveBulkItemParams( + $singleEntityParams, + $routeServiceClass, +@@ -143,11 +162,22 @@ public function getInputData() + $inputData = $this->request->getRequestData(); + + $httpMethod = $this->request->getHttpMethod(); +- if ($httpMethod == RestRequest::HTTP_METHOD_DELETE) { ++ if ($httpMethod === RestRequest::HTTP_METHOD_DELETE) { + $requestBodyParams = $this->request->getBodyParams(); + $inputData = array_merge($requestBodyParams, $inputData); + } +- return $inputData; ++ ++ return array_map(function ($singleEntityParams) { ++ if (is_array($singleEntityParams)) { ++ $singleEntityParams = $this->filterInputData($singleEntityParams); ++ $singleEntityParams = $this->paramsOverrider->override( ++ $singleEntityParams, ++ $this->getRoute()->getParameters() ++ ); ++ } ++ ++ return $singleEntityParams; ++ }, $inputData); + } + + /** +@@ -180,4 +210,65 @@ private function resolveBulkItemParams(array $inputData, string $serviceClass, s + { + return $this->serviceInputProcessor->process($serviceClass, $serviceMethod, $inputData); + } ++ ++ /** ++ * Validates InputData ++ * ++ * @param array $inputData ++ * @return array ++ */ ++ private function filterInputData(array $inputData): array ++ { ++ $result = []; ++ ++ $data = array_filter($inputData, function ($k) use (&$result) { ++ $key = is_string($k) ? strtolower(str_replace('_', "", $k)) : $k; ++ return !isset($result[$key]) && ($result[$key] = true); ++ }, ARRAY_FILTER_USE_KEY); ++ ++ return array_map(function ($value) { ++ return is_array($value) ? $this->filterInputData($value) : $value; ++ }, $data); ++ } ++ ++ /** ++ * Validate that parameters are really used in the current request. ++ * ++ * @param string $serviceClassName ++ * @param string $serviceMethodName ++ * @param array $paramOverriders ++ */ ++ private function validateParameters( ++ string $serviceClassName, ++ string $serviceMethodName, ++ array $paramOverriders ++ ): void { ++ //phpcs:ignore CopyPaste ++ $methodParams = $this->methodsMap->getMethodParams($serviceClassName, $serviceMethodName); ++ foreach ($paramOverriders as $key => $param) { ++ $arrayKeys = explode('.', $param ?? ''); ++ $value = array_shift($arrayKeys); ++ ++ foreach ($methodParams as $serviceMethodParam) { ++ $serviceMethodParamName = $serviceMethodParam[MethodsMap::METHOD_META_NAME]; ++ $serviceMethodType = $serviceMethodParam[MethodsMap::METHOD_META_TYPE]; ++ ++ $camelCaseValue = SimpleDataObjectConverter::snakeCaseToCamelCase($value); ++ if ($serviceMethodParamName === $value || $serviceMethodParamName === $camelCaseValue) { ++ if (count($arrayKeys) > 0) { ++ $camelCaseKey = SimpleDataObjectConverter::snakeCaseToCamelCase('set_' . $arrayKeys[0]); ++ $this->validateParameters($serviceMethodType, $camelCaseKey, [implode('.', $arrayKeys)]); ++ } ++ unset($paramOverriders[$key]); ++ break; ++ } ++ } ++ } ++ ++ if (!empty($paramOverriders)) { ++ $message = 'The current request does not expect the next parameters: ' ++ . implode(', ', $paramOverriders); ++ throw new \UnexpectedValueException(__($message)->__toString()); ++ } ++ } + } +diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +index 68cc2c2b2315..6f08b21f3812 100644 +--- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php ++++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +@@ -354,7 +354,7 @@ public function testPlaceOrder() + public function testAssignCustomerByGuestUser() + { + $this->expectException(\Exception::class); +- $this->expectExceptionMessage('You don\'t have the correct permissions to assign the customer to the cart.'); ++ $this->expectExceptionMessage('Enter and try again.'); + + /** @var $quote \Magento\Quote\Model\Quote */ + $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); +diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +index 08ba4bba28c6..c9d07aa2abed 100644 +--- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt ++++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +@@ -111,3 +111,4 @@ app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/ed + app/code/Magento/GoogleGtag + app/code/Magento/AdminAdobeIms/Observer/AuthObserver + app/code/Magento/OpenSearch/SearchAdapter/Adapter ++app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php + diff --git a/patches/MCLOUD-13605__B2B_SQL_syntax_error_due_to_the_REGEXP_LIKE_function__1.5.2.patch b/patches/MCLOUD-13605__B2B_SQL_syntax_error_due_to_the_REGEXP_LIKE_function__1.5.2.patch new file mode 100644 index 00000000..33e5683a --- /dev/null +++ b/patches/MCLOUD-13605__B2B_SQL_syntax_error_due_to_the_REGEXP_LIKE_function__1.5.2.patch @@ -0,0 +1,14 @@ +diff --git a/vendor/magento/module-company/Setup/Patch/Data/SetCompanyForStructure.php b/vendor/magento/module-company/Setup/Patch/Data/SetCompanyForStructure.php +index 4dcb3dbcb5b3..7ba339a8703e 100644 +--- a/vendor/magento/module-company/Setup/Patch/Data/SetCompanyForStructure.php ++++ b/vendor/magento/module-company/Setup/Patch/Data/SetCompanyForStructure.php +@@ -71,7 +71,7 @@ public function apply() + $this->moduleDataSetup->getConnection()->update( + $this->moduleDataSetup->getTable('company_structure'), + ['company_id' => $company['entity_id']], +- ['REGEXP_LIKE(path, ?)' => ++ ['path REGEXP ?' => + '^' . $adminStructureIds[$company['super_user_id']]['structure_id'] . '(/.+)?$'] + ); + } + diff --git a/patches/MCLOUD-13619__Improve_web_api_async_performance__2.4.x.patch b/patches/MCLOUD-13619__Improve_web_api_async_performance__2.4.x.patch new file mode 100644 index 00000000..3073c351 --- /dev/null +++ b/patches/MCLOUD-13619__Improve_web_api_async_performance__2.4.x.patch @@ -0,0 +1,50 @@ +diff --git a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +index 93555559ac9a1..2f46685c6b117 100644 +--- a/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php ++++ b/vendor/magento/module-webapi-async/Controller/Rest/Asynchronous/InputParamsResolver.php +@@ -70,6 +70,11 @@ class InputParamsResolver + */ + private $methodsMap; + ++ /** ++ * @var array ++ */ ++ private array $inputData = []; ++ + /** + * Initialize dependencies. + * +@@ -156,8 +161,14 @@ public function resolve() + */ + public function getInputData() + { ++ if (!empty($this->inputData)) { ++ return $this->inputData; ++ } ++ + if ($this->isBulk === false) { +- return [$this->inputParamsResolver->getInputData()]; ++ $this->inputData = [$this->inputParamsResolver->getInputData()]; ++ ++ return $this->inputData; + } + $inputData = $this->request->getRequestData(); + +@@ -167,7 +178,7 @@ public function getInputData() + $inputData = array_merge($requestBodyParams, $inputData); + } + +- return array_map(function ($singleEntityParams) { ++ $this->inputData = array_map(function ($singleEntityParams) { + if (is_array($singleEntityParams)) { + $singleEntityParams = $this->filterInputData($singleEntityParams); + $singleEntityParams = $this->paramsOverrider->override( +@@ -178,6 +189,8 @@ public function getInputData() + + return $singleEntityParams; + }, $inputData); ++ ++ return $this->inputData; + } + + /** diff --git a/patches/MCLOUD-13752__Patch_for_CVE-2025-47109_Improve_category_view__2.4.8.patch b/patches/MCLOUD-13752__Patch_for_CVE-2025-47109_Improve_category_view__2.4.8.patch new file mode 100644 index 00000000..e042d0ba --- /dev/null +++ b/patches/MCLOUD-13752__Patch_for_CVE-2025-47109_Improve_category_view__2.4.8.patch @@ -0,0 +1,59 @@ +diff --git a/vendor/magento/module-catalog/Helper/Category.php b/vendor/magento/module-catalog/Helper/Category.php +index fe511d40e9caa..761dc6f62adda 100644 +--- a/vendor/magento/module-catalog/Helper/Category.php ++++ b/vendor/magento/module-catalog/Helper/Category.php +@@ -10,8 +10,10 @@ + use Magento\Catalog\Model\CategoryFactory; + use Magento\Framework\App\Helper\AbstractHelper; + use Magento\Framework\App\Helper\Context; ++use Magento\Framework\App\ObjectManager; + use Magento\Framework\Data\CollectionFactory; + use Magento\Framework\Data\Tree\Node\Collection; ++use Magento\Framework\Escaper; + use Magento\Framework\Exception\NoSuchEntityException; + use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + use Magento\Store\Model\ScopeInterface; +@@ -63,24 +65,33 @@ class Category extends AbstractHelper implements ResetAfterRequestInterface + */ + protected $categoryRepository; + ++ /** ++ * @var Escaper|null ++ */ ++ private ?Escaper $escaper; ++ + /** + * @param Context $context + * @param CategoryFactory $categoryFactory + * @param StoreManagerInterface $storeManager + * @param CollectionFactory $dataCollectionFactory + * @param CategoryRepositoryInterface $categoryRepository ++ * @param Escaper|null $escaper + */ + public function __construct( + Context $context, + CategoryFactory $categoryFactory, + StoreManagerInterface $storeManager, + CollectionFactory $dataCollectionFactory, +- CategoryRepositoryInterface $categoryRepository ++ CategoryRepositoryInterface $categoryRepository, ++ ?Escaper $escaper = null + ) { + $this->_categoryFactory = $categoryFactory; + $this->_storeManager = $storeManager; + $this->_dataCollectionFactory = $dataCollectionFactory; + $this->categoryRepository = $categoryRepository; ++ $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); ++ + parent::__construct($context); + } + +@@ -204,6 +215,7 @@ public function getCanonicalUrl(string $categoryUrl): string + if ($params && isset($params['p'])) { + $categoryUrl = $categoryUrl . '?p=' . $params['p']; + } +- return $categoryUrl; ++ ++ return $this->escaper->escapeUrl($categoryUrl); + } + } diff --git a/patches/MCLOUD-13753__Patch_for_CVE-2025-47110_improve-admin-cache-efficiency__2.4.x.patch b/patches/MCLOUD-13753__Patch_for_CVE-2025-47110_improve-admin-cache-efficiency__2.4.x.patch new file mode 100644 index 00000000..b9af7660 --- /dev/null +++ b/patches/MCLOUD-13753__Patch_for_CVE-2025-47110_improve-admin-cache-efficiency__2.4.x.patch @@ -0,0 +1,25 @@ +diff --git a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php +index f5b69484285cc..f86deacad3b4e 100644 +--- a/vendor/magento/module-email/Model/Template/Filter.php ++++ b/vendor/magento/module-email/Model/Template/Filter.php +@@ -1,7 +1,7 @@ + setDataUsingMethod($k, $v); + } + ++ if (!$block->hasData('cache_key')) { ++ $block->setDataUsingMethod('cache_key', $block->getCacheKey()); ++ } ++ + if (isset($blockParameters['output'])) { + $method = $blockParameters['output']; + } diff --git a/src/App/GenericException.php b/src/App/GenericException.php index 06955e80..60fbd9f4 100644 --- a/src/App/GenericException.php +++ b/src/App/GenericException.php @@ -19,7 +19,7 @@ class GenericException extends \Exception * @param int $code * @param Throwable|null $previous */ - public function __construct(string $message, int $code = 0, Throwable $previous = null) + public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/src/Command/Apply.php b/src/Command/Apply.php index 47b1692e..2fb110cf 100644 --- a/src/Command/Apply.php +++ b/src/Command/Apply.php @@ -81,7 +81,7 @@ protected function configure() /** * @inheritDoc */ - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $this->logger->info($this->magentoVersion->get()); diff --git a/src/Command/Ece/Apply.php b/src/Command/Ece/Apply.php index 7a1b513e..6749962c 100644 --- a/src/Command/Ece/Apply.php +++ b/src/Command/Ece/Apply.php @@ -89,7 +89,7 @@ protected function configure() /** * @inheritDoc */ - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $this->logger->info($this->magentoVersion->get()); diff --git a/src/Command/Ece/Revert.php b/src/Command/Ece/Revert.php index 82446359..5065008a 100644 --- a/src/Command/Ece/Revert.php +++ b/src/Command/Ece/Revert.php @@ -71,7 +71,7 @@ protected function configure() /** * {@inheritDoc} */ - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $this->logger->info($this->magentoVersion->get()); diff --git a/src/Command/Revert.php b/src/Command/Revert.php index d0bc7e40..70412826 100644 --- a/src/Command/Revert.php +++ b/src/Command/Revert.php @@ -94,7 +94,7 @@ protected function configure() /** * {@inheritDoc} */ - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $this->logger->info($this->magentoVersion->get()); diff --git a/src/Command/Status.php b/src/Command/Status.php index 72df3a05..3f70ed97 100644 --- a/src/Command/Status.php +++ b/src/Command/Status.php @@ -61,7 +61,7 @@ protected function configure() /** * {@inheritDoc} */ - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { try { $this->showStatus->run($input, $output); diff --git a/src/Console/QuestionFactory.php b/src/Console/QuestionFactory.php index 9a21dfd4..d45f7417 100644 --- a/src/Console/QuestionFactory.php +++ b/src/Console/QuestionFactory.php @@ -22,7 +22,7 @@ class QuestionFactory * * @return Question */ - public function create(string $question, string $default = null): Question + public function create(string $question, ?string $default = null): Question { return new Question($question, $default); } diff --git a/src/Shell/ProcessFactory.php b/src/Shell/ProcessFactory.php index 5184bea2..4cb7a3fc 100644 --- a/src/Shell/ProcessFactory.php +++ b/src/Shell/ProcessFactory.php @@ -49,7 +49,7 @@ public function __construct(DirectoryList $directoryList, Composer $composer) * @return Process * @throws PackageNotFoundException */ - public function create(array $cmd, string $input = null): Process + public function create(array $cmd, ?string $input = null): Process { return new Process( $this->processSupportsArrayParam() ? $cmd : implode(' ', $cmd), diff --git a/src/Test/Functional/Acceptance/AbstractCest.php b/src/Test/Functional/Acceptance/AbstractCest.php index d3374bbc..07c9c213 100644 --- a/src/Test/Functional/Acceptance/AbstractCest.php +++ b/src/Test/Functional/Acceptance/AbstractCest.php @@ -17,6 +17,123 @@ class AbstractCest */ protected $edition = 'EE'; + /** + * @var array + */ + private $dependencyListFor244 = [ + "magento/module-re-captcha-admin-ui" => "1.1.2", + "magento/module-re-captcha-checkout" => "1.1.2", + "magento/module-re-captcha-contact" => "1.1.1", + "magento/module-re-captcha-customer" => "1.1.2", + "magento/module-re-captcha-frontend-ui" => "1.1.2", + "magento/module-re-captcha-migration" => "1.1.2", + "magento/module-re-captcha-newsletter" => "1.1.2", + "magento/module-re-captcha-paypal" => "1.1.2", + "magento/module-re-captcha-review" => "1.1.2", + "magento/module-re-captcha-send-friend" => "1.1.2", + "magento/module-re-captcha-store-pickup" => "1.0.1", + "magento/module-re-captcha-ui" => "1.1.2", + "magento/module-re-captcha-user" => "1.1.2", + "magento/module-re-captcha-validation" => "1.1.1", + "magento/module-re-captcha-validation-api" => "1.1.1", + "magento/module-re-captcha-version-2-checkbox" => "2.0.2", + "magento/module-re-captcha-version-2-invisible" => "2.0.2", + "magento/module-re-captcha-version-3-invisible" => "2.0.2", + "magento/module-re-captcha-webapi-api" => "1.0.1", + "magento/module-re-captcha-webapi-rest" => "1.0.1", + "magento/module-re-captcha-webapi-graph-ql" => "1.0.1", + "magento/module-re-captcha-webapi-ui" => "1.0.1", + "magento/module-securitytxt" => "1.1.1", + "magento/module-two-factor-auth" => "1.1.3", + "magento/module-re-captcha-checkout-sales-rule" => "1.1.0", + "magento/inventory-composer-installer" => "1.2.0", + "magento/module-inventory" => "1.2.2", + "magento/module-inventory-admin-ui" => "1.2.2", + "magento/module-inventory-advanced-checkout" => "1.2.1", + "magento/module-inventory-api" => "1.2.2", + "magento/module-inventory-bundle-product" => "1.2.1", + "magento/module-inventory-bundle-product-admin-ui" => "1.2.2", + "magento/module-inventory-bundle-product-indexer" => "1.1.1", + "magento/module-inventory-bundle-import-export" => "1.1.1", + "magento/module-inventory-cache" => "1.2.2", + "magento/module-inventory-catalog" => "1.2.2", + "magento/module-inventory-catalog-admin-ui" => "1.2.2", + "magento/module-inventory-catalog-api" => "1.3.2", + "magento/module-inventory-catalog-search" => "1.2.2", + "magento/module-inventory-configurable-product" => "1.2.2", + "magento/module-inventory-configurable-product-admin-ui" => "1.2.2", + "magento/module-inventory-configurable-product-indexer" => "1.2.2", + "magento/module-inventory-configuration" => "1.2.2", + "magento/module-inventory-configuration-api" => "1.2.1", + "magento/module-inventory-distance-based-source-selection" => "1.2.2", + "magento/module-inventory-distance-based-source-selection-admin-ui" => "1.2.1", + "magento/module-inventory-distance-based-source-selection-api" => "1.2.1", + "magento/module-inventory-elasticsearch" => "1.2.1", + "magento/module-inventory-export-stock" => "1.2.1", + "magento/module-inventory-export-stock-api" => "1.2.1", + "magento/module-inventory-graph-ql" => "1.2.1", + "magento/module-inventory-grouped-product" => "1.2.2", + "magento/module-inventory-grouped-product-admin-ui" => "1.2.2", + "magento/module-inventory-grouped-product-indexer" => "1.2.2", + "magento/module-inventory-import-export" => "1.2.2", + "magento/module-inventory-indexer" => "2.1.2", + "magento/module-inventory-in-store-pickup" => "1.1.1", + "magento/module-inventory-in-store-pickup-admin-ui" => "1.1.1", + "magento/module-inventory-in-store-pickup-api" => "1.1.1", + "magento/module-inventory-in-store-pickup-frontend" => "1.1.2", + "magento/module-inventory-in-store-pickup-graph-ql" => "1.1.1", + "magento/module-inventory-in-store-pickup-multishipping" => "1.1.1", + "magento/module-inventory-in-store-pickup-quote" => "1.1.1", + "magento/module-inventory-in-store-pickup-quote-graph-ql" => "1.1.1", + "magento/module-inventory-in-store-pickup-sales" => "1.1.1", + "magento/module-inventory-in-store-pickup-sales-admin-ui" => "1.1.2", + "magento/module-inventory-in-store-pickup-sales-api" => "1.1.1", + "magento/module-inventory-in-store-pickup-shipping" => "1.1.1", + "magento/module-inventory-in-store-pickup-shipping-admin-ui" => "1.1.1", + "magento/module-inventory-in-store-pickup-shipping-api" => "1.1.1", + "magento/module-inventory-in-store-pickup-webapi-extension" => "1.1.1", + "magento/module-inventory-low-quantity-notification" => "1.2.1", + "magento/module-inventory-low-quantity-notification-admin-ui" => "1.2.2", + "magento/module-inventory-low-quantity-notification-api" => "1.2.1", + "magento/module-inventory-multi-dimensional-indexer-api" => "1.2.1", + "magento/module-inventory-product-alert" => "1.2.2", + "magento/module-inventory-quote-graph-ql" => "1.0.1", + "magento/module-inventory-requisition-list" => "1.2.2", + "magento/module-inventory-reservation-cli" => "1.2.2", + "magento/module-inventory-reservations" => "1.2.1", + "magento/module-inventory-reservations-api" => "1.2.1", + "magento/module-inventory-sales" => "1.2.2", + "magento/module-inventory-sales-admin-ui" => "1.2.2", + "magento/module-inventory-sales-api" => "1.2.1", + "magento/module-inventory-sales-frontend-ui" => "1.2.2", + "magento/module-inventory-setup-fixture-generator" => "1.2.1", + "magento/module-inventory-shipping" => "1.2.2", + "magento/module-inventory-shipping-admin-ui" => "1.2.2", + "magento/module-inventory-source-deduction-api" => "1.2.2", + "magento/module-inventory-source-selection" => "1.2.1", + "magento/module-inventory-source-selection-api" => "1.4.1", + "magento/module-inventory-visual-merchandiser" => "1.1.2", + "magento/module-inventory-swatches-frontend-ui" => "1.0.1", + "magento/module-inventory-catalog-frontend-ui" => "1.0.2", + "magento/module-inventory-configurable-product-frontend-ui" => "1.0.2", + "magento/module-inventory-wishlist" => "1.0.1", + "magento/module-inventory-catalog-search-bundle-product" => "1.0.1", + "magento/module-inventory-catalog-search-configurable-product" => "1.0.1", + "magento/module-page-builder" => "2.2.2", + "magento/module-page-builder-analytics" => "1.6.2", + "magento/module-cms-page-builder-analytics" => "1.6.2", + "magento/module-page-builder-admin-analytics" => "1.1.2", + "magento/module-catalog-page-builder-analytics" => "1.6.2", + "magento/module-aws-s3-page-builder" => "1.0.2", + "magento/module-banner-page-builder" => "2.2.2", + "magento/module-banner-page-builder-analytics" => "1.7.1", + "magento/module-catalog-staging-page-builder" => "1.7.1", + "magento/module-staging-page-builder" => "2.2.2", + "magento/module-cms-page-builder-analytics-staging" => "1.7.1", + "magento/module-catalog-page-builder-analytics-staging" => "1.7.1", + "magento/module-page-builder-admin-gws-admin-ui" => "1.7.1" + ]; + /** * @param \CliTester $I */ @@ -29,6 +146,7 @@ public function _before(\CliTester $I): void * @param \CliTester $I * @param string $templateVersion * @param string $magentoVersion + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function prepareTemplate(\CliTester $I, string $templateVersion, string $magentoVersion = null): void { @@ -58,6 +176,15 @@ protected function prepareTemplate(\CliTester $I, string $templateVersion, strin ); } + if ($magentoVersion === '2.4.4') { + foreach ($this->dependencyListFor244 as $package => $version) { + $I->assertTrue( + $I->addDependencyToComposer($package, $version), + "Can not override dependency {$package} with version {$version} for Adobe Commerce 2.4.4" + ); + } + } + if ($this->edition === 'CE' || $magentoVersion) { $version = $magentoVersion ?: $this->getVersionRangeForMagento($I); $I->removeDependencyFromComposer('magento/magento-cloud-metapackage'); diff --git a/src/Test/Functional/Acceptance/Acceptance81Cest.php b/src/Test/Functional/Acceptance/Acceptance81Cest.php index 49ffd695..bbd08f33 100644 --- a/src/Test/Functional/Acceptance/Acceptance81Cest.php +++ b/src/Test/Functional/Acceptance/Acceptance81Cest.php @@ -18,14 +18,22 @@ class Acceptance81Cest extends AcceptanceCest protected function patchesDataProvider(): array { return [ - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p1'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p2'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p3'], - ['templateVersion' => '2.4.4', 'magentoVersion' => '2.4.4-p4'], - ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5'], - ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5-p1'], - ['templateVersion' => '2.4.5', 'magentoVersion' => '2.4.5-p2'], + ['templateVersion' => '2.4.4-p1-p8', 'magentoVersion' => '2.4.4-p1'], + ['templateVersion' => '2.4.4-p1-p8', 'magentoVersion' => '2.4.4-p2'], + ['templateVersion' => '2.4.4-p1-p8', 'magentoVersion' => '2.4.4-p3'], + ['templateVersion' => '2.4.4-p1-p8', 'magentoVersion' => '2.4.4-p4'], + ['templateVersion' => '2.4.4-p1-p8', 'magentoVersion' => '2.4.4-p5'], + ['templateVersion' => '2.4.4-p1-p8', 'magentoVersion' => '2.4.4-p6'], + ['templateVersion' => '2.4.4-p1-p8', 'magentoVersion' => '2.4.4-p7'], + ['templateVersion' => '2.4.4-p1-p8', 'magentoVersion' => '2.4.4-p8'], + ['templateVersion' => '2.4.5-p1-p7', 'magentoVersion' => '2.4.5'], + ['templateVersion' => '2.4.5-p1-p7', 'magentoVersion' => '2.4.5-p1'], + ['templateVersion' => '2.4.5-p1-p7', 'magentoVersion' => '2.4.5-p2'], + ['templateVersion' => '2.4.5-p1-p7', 'magentoVersion' => '2.4.5-p3'], + ['templateVersion' => '2.4.5-p1-p7', 'magentoVersion' => '2.4.5-p4'], + ['templateVersion' => '2.4.5-p1-p7', 'magentoVersion' => '2.4.5-p5'], + ['templateVersion' => '2.4.5-p1-p7', 'magentoVersion' => '2.4.5-p6'], + ['templateVersion' => '2.4.5-p1-p7', 'magentoVersion' => '2.4.5-p7'], ]; } } diff --git a/src/Test/Functional/Acceptance/Acceptance82Cest.php b/src/Test/Functional/Acceptance/Acceptance82Cest.php new file mode 100644 index 00000000..4433514b --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance82Cest.php @@ -0,0 +1,26 @@ + '2.4.6', 'magentoVersion' => '2.4.6'], + ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6-p1'], + ['templateVersion' => '2.4.7', 'magentoVersion' => null], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/Acceptance83Cest.php b/src/Test/Functional/Acceptance/Acceptance83Cest.php new file mode 100644 index 00000000..412ad871 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance83Cest.php @@ -0,0 +1,24 @@ + '2.4.7', 'magentoVersion' => '2.4.7'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/Acceptance84Cest.php b/src/Test/Functional/Acceptance/Acceptance84Cest.php new file mode 100644 index 00000000..fac30028 --- /dev/null +++ b/src/Test/Functional/Acceptance/Acceptance84Cest.php @@ -0,0 +1,24 @@ + '2.4.8', 'magentoVersion' => '2.4.8'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/AcceptanceCest.php b/src/Test/Functional/Acceptance/AcceptanceCest.php index 79cd3217..070482ac 100644 --- a/src/Test/Functional/Acceptance/AcceptanceCest.php +++ b/src/Test/Functional/Acceptance/AcceptanceCest.php @@ -8,9 +8,11 @@ namespace Magento\CloudPatches\Test\Functional\Acceptance; /** - * @group php82 + * Abstract AcceptanceCest + * + * @abstract */ -class AcceptanceCest extends AbstractCest +abstract class AcceptanceCest extends AbstractCest { /** * @param \CliTester $I @@ -46,12 +48,5 @@ public function testPatches(\CliTester $I, \Codeception\Example $data): void /** * @return array */ - protected function patchesDataProvider(): array - { - return [ - ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6'], - ['templateVersion' => '2.4.6', 'magentoVersion' => '2.4.6-p1'], - ['templateVersion' => '2.4.7-beta', 'magentoVersion' => null], - ]; - } + abstract protected function patchesDataProvider(): array; } diff --git a/src/Test/Functional/Acceptance/PatchApplier83Cest.php b/src/Test/Functional/Acceptance/PatchApplier83Cest.php new file mode 100644 index 00000000..49e3e4b9 --- /dev/null +++ b/src/Test/Functional/Acceptance/PatchApplier83Cest.php @@ -0,0 +1,24 @@ + '2.4.7', 'magentoVersion' => '2.4.7'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/PatchApplier84Cest.php b/src/Test/Functional/Acceptance/PatchApplier84Cest.php new file mode 100644 index 00000000..c14a280c --- /dev/null +++ b/src/Test/Functional/Acceptance/PatchApplier84Cest.php @@ -0,0 +1,24 @@ + '2.4.8', 'magentoVersion' => '2.4.8'], + ]; + } +} diff --git a/src/Test/Functional/Acceptance/PatchApplierCest.php b/src/Test/Functional/Acceptance/PatchApplierCest.php index 0b5b6e80..46b526d6 100644 --- a/src/Test/Functional/Acceptance/PatchApplierCest.php +++ b/src/Test/Functional/Acceptance/PatchApplierCest.php @@ -10,28 +10,43 @@ use Magento\CloudDocker\Test\Functional\Codeception\Docker; /** - * @group php82 + * Abstract PatchApplierCest + * + * @abstract */ -class PatchApplierCest extends AbstractCest +abstract class PatchApplierCest extends AbstractCest { /** - * @param \CliTester $I + * Prepares the test environment before each test. + * + * @param \CliTester $I The CLI tester instance. + * @throws \Robo\Exception\TaskException */ public function _before(\CliTester $I): void { parent::_before($I); - - $this->prepareTemplate($I, '2.4.6'); - $I->copyFileToWorkDir('files/debug_logging/.magento.env.yaml', '.magento.env.yaml'); } /** + * Tests applying an existing patch to a target file. + * * @param \CliTester $I + * @param \Codeception\Example $data The example data for the test. + * Expected structure: + * [ + * 'templateVersion' => string, + * 'magentoVersion' => string|null (optional) + * ] * @throws \Robo\Exception\TaskException + * @dataProvider patchesDataProvider */ - public function testApplyingPatch(\CliTester $I): void + public function testApplyingPatch(\CliTester $I, \Codeception\Example $data): void { + $this->prepareTemplate($I, $data['templateVersion'], $data['magentoVersion'] ?? null); + $I->generateDockerCompose('--mode=production'); + + $I->copyFileToWorkDir('files/debug_logging/.magento.env.yaml', '.magento.env.yaml'); $I->copyFileToWorkDir('files/patches/target_file.md', 'target_file.md'); $I->copyFileToWorkDir('files/patches/patch.patch', 'm2-hotfixes/patch.patch'); @@ -42,17 +57,30 @@ public function testApplyingPatch(\CliTester $I): void $targetFile = $I->grabFileContent('/target_file.md', Docker::BUILD_CONTAINER); $I->assertStringContainsString('# Hello Magento', $targetFile); $I->assertStringContainsString('## Additional Info', $targetFile); - $log = $I->grabFileContent('/var/log/cloud.log', Docker::BUILD_CONTAINER); + $log = $I->grabFileContent('/init/var/log/cloud.log', Docker::BUILD_CONTAINER); $I->assertStringContainsString('Patch ../m2-hotfixes/patch.patch has been applied', $log); } /** + * Tests that an existing patch is not applied again. + * * @param \CliTester $I + * @param \Codeception\Example $data The example data for the test. + * Expected structure: + * [ + * 'templateVersion' => string, + * 'magentoVersion' => string|null (optional) + * ] * @throws \Robo\Exception\TaskException + * @dataProvider patchesDataProvider */ - public function testApplyingExistingPatch(\CliTester $I): void + public function testApplyingExistingPatch(\CliTester $I, \Codeception\Example $data): void { + $this->prepareTemplate($I, $data['templateVersion'], $data['magentoVersion'] ?? null); + $I->generateDockerCompose('--mode=production'); + + $I->copyFileToWorkDir('files/debug_logging/.magento.env.yaml', '.magento.env.yaml'); $I->copyFileToWorkDir('files/patches/target_file_applied_patch.md', 'target_file.md'); $I->copyFileToWorkDir('files/patches/patch.patch', 'm2-hotfixes/patch.patch'); @@ -65,7 +93,13 @@ public function testApplyingExistingPatch(\CliTester $I): void $I->assertStringContainsString('## Additional Info', $targetFile); $I->assertStringContainsString( 'Patch ../m2-hotfixes/patch.patch was already applied', - $I->grabFileContent('/var/log/cloud.log', Docker::BUILD_CONTAINER) + $I->grabFileContent('/init/var/log/cloud.log', Docker::BUILD_CONTAINER) ); } + + /** + * Returns the data provider for patches. + * @return array + */ + abstract protected function patchesDataProvider(): array; } diff --git a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php index eb0c62ff..6c8ae3fd 100644 --- a/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ApplyOptionalActionTest.php @@ -108,7 +108,12 @@ public function testExecuteSuccessful() $outputMock = $this->getMockForAbstractClass(OutputInterface::class); $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patch1, $patch2, $patch3]); $this->applier->method('apply') @@ -117,18 +122,27 @@ public function testExecuteSuccessful() [$patch2->getPath(), $patch2->getId(), 'Patch ' . $patch2->getId() .' has been applied'], [$patch3->getPath(), $patch3->getId(), 'Patch ' . $patch3->getId() .' has been applied'], ]); - $this->renderer->expects($this->exactly(3)) - ->method('printPatchInfo') - ->withConsecutive( - [$outputMock, $patch1, 'Patch ' . $patch1->getId() .' has been applied'], - [$outputMock, $patch2, 'Patch ' . $patch2->getId() .' has been applied'], - [$outputMock, $patch3, 'Patch ' . $patch3->getId() .' has been applied'] - ); - + ->method('printPatchInfo') + ->willReturnCallback(function ($patch, $message) use ($patch1, $patch2, $patch3) { + static $callCount = 0; + $expectedPatches = [$patch1, $patch2, $patch3]; + $expectedMessages = [ + 'Patch ' . $patch1->getId() . ' has been applied', + 'Patch ' . $patch2->getId() . ' has been applied', + 'Patch ' . $patch3->getId() . ' has been applied' + ]; + + if ($patch === $expectedPatches[$callCount] && $message === $expectedMessages[$callCount]) { + $callCount++; + return true; + } + + return false; + }); $this->action->execute($inputMock, $outputMock, $patchFilter); } - + /** * Tests successful optional patches applying. * @@ -149,9 +163,13 @@ public function testApplyAlreadyAppliedPatch() $outputMock = $this->getMockForAbstractClass(OutputInterface::class); $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patch1]); - $this->applier->expects($this->never()) ->method('apply'); $this->renderer->expects($this->never()) @@ -159,12 +177,10 @@ public function testApplyAlreadyAppliedPatch() $outputMock->expects($this->once()) ->method('writeln') - ->withConsecutive( - [ - $this->stringContains( - 'Patch ' . $patch1->getId() .' (' . $patch1->getFilename() . ') was already applied' - ) - ] + ->with( + $this->stringContains( + 'Patch ' . $patch1->getId() .' (' . $patch1->getFilename() . ') was already applied' + ) ); $this->action->execute($inputMock, $outputMock, $patchFilter); @@ -203,8 +219,10 @@ public function testApplyingAllPatchesAndSkipDeprecated() $this->renderer->expects($this->once()) ->method('printPatchInfo') - ->withConsecutive( - [$outputMock, $patch1, 'Patch ' . $patch1->getId() .' has been applied'] + ->with( + $outputMock, + $patch1, + 'Patch ' . $patch1->getId() .' has been applied' ); $this->action->execute($inputMock, $outputMock, $patchFilter); @@ -232,27 +250,35 @@ public function testApplyWithException() ->willReturn([$patch1, $patch2]); $this->applier->method('apply') - ->willReturnMap([ - [$patch1->getPath(), $patch1->getId()], - [$patch2->getPath(), $patch2->getId()] - ])->willReturnCallback( - function ($path, $id) { - if ($id === 'MC-22222') { - throw new ApplierException('Applier error message'); - } - - return "Patch {$path} {$id} has been applied"; + ->willReturnCallback(function ($path, $id) use ($patch1, $patch2) { + if ($id === 'MC-22222') { + throw new ApplierException('Applier error message'); } - ); - + // Return success message for the first patch + return "Patch {$path} {$id} has been applied"; + }); $this->conflictProcessor->expects($this->once()) ->method('process') - ->withConsecutive([$outputMock, $patch2, [$patch1], 'Applier error message']) + ->willReturnCallback(function ($patch, $message) use ($patch1, $patch2, $patch3) { + static $callCount = 0; + $expectedPatches = [$patch1, $patch2, $patch3]; + $expectedMessages = [ + 'Patch ' . $patch1->getId() . ' has been applied', + 'Patch ' . $patch2->getId() . ' has been applied', + 'Patch ' . $patch3->getId() . ' has been applied' + ]; + + if ($patch === $expectedPatches[$callCount] && $message === $expectedMessages[$callCount]) { + $callCount++; + return true; + } + + return false; + }) ->willThrowException(new RuntimeException('Error message')); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Error message'); - $this->action->execute($inputMock, $outputMock, $patchFilter); } diff --git a/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php b/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php index 2d374ca9..aecea225 100644 --- a/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ConfirmRequiredActionTest.php @@ -86,15 +86,20 @@ public function testAskConfirmationForNotAppliedPatches() ]); /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->optionalPool->expects($this->once()) ->method('getAdditionalRequiredPatches') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patch1, $patch2, $patch3]); - $aggregatedPatch = $this->getMockForAbstractClass(AggregatedPatchInterface::class); + $aggregatedPatch = $this->createMock(AggregatedPatchInterface::class); $this->aggregator->expects($this->once()) ->method('aggregate') ->with([$patch1, $patch2]) @@ -102,8 +107,13 @@ public function testAskConfirmationForNotAppliedPatches() $this->renderer->expects($this->once()) ->method('printTable') - ->withConsecutive([$outputMock, [$aggregatedPatch]]); - + ->with($outputMock, [$aggregatedPatch]) + ->willReturnCallback(function ($output) use ($outputMock, $aggregatedPatch) { + if ($output === $outputMock && $aggregatedPatch === [$aggregatedPatch]) { + throw new RuntimeException('Error message'); + } + return null; + }); $this->renderer->expects($this->once()) ->method('printQuestion') ->willReturn(true); @@ -119,12 +129,12 @@ public function testPatchNotFoundException() $patchFilter = ['unknown id']; /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); - $this->optionalPool->expects($this->once()) + $outputMock = $this->createMock(OutputInterface::class); + $this->optionalPool->expects($this->once()) ->method('getAdditionalRequiredPatches') - ->withConsecutive([$patchFilter]) + ->with($patchFilter) ->willThrowException(new PatchNotFoundException('')); $this->expectException(RuntimeException::class); @@ -144,23 +154,29 @@ public function testConfirmationRejected() ]); /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->optionalPool->expects($this->once()) ->method('getAdditionalRequiredPatches') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patch1]); - $aggregatedPatch = $this->getMockForAbstractClass(AggregatedPatchInterface::class); + $aggregatedPatch = $this->createMock(AggregatedPatchInterface::class); $this->aggregator->expects($this->once()) ->method('aggregate') ->with([$patch1]) ->willReturn([$aggregatedPatch]); - $this->renderer->expects($this->once()) - ->method('printTable') - ->withConsecutive([$outputMock, [$aggregatedPatch]]); + $this->optionalPool->expects($this->once()) + ->method('getAdditionalRequiredPatches') + ->with($patchFilter) + ->willReturn([$patch1]); $this->renderer->expects($this->once()) ->method('printQuestion') @@ -181,7 +197,7 @@ public function testConfirmationRejected() */ private function createPatch(string $path, string $id, bool $isDeprecated = false) { - $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch = $this->createMock(PatchInterface::class); $patch->method('getPath')->willReturn($path); $patch->method('getFilename')->willReturn('filename.patch'); $patch->method('getId')->willReturn($id); diff --git a/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php b/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php index 441a0722..70b6cdf7 100644 --- a/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ProcessDeprecatedActionTest.php @@ -105,11 +105,21 @@ public function testProcessDeprecationSuccessful() $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patchMock]); $this->optionalPool->expects($this->once()) ->method('getReplacedBy') - ->withConsecutive([$patch1->getId()]) + ->willReturnCallback(function ($patchId) use ($patchFilter, $patch1) { + if ($patchId === $patch1->getId()) { + return [$patch1]; + } + return []; + }) ->willReturn([]); $this->aggregator->expects($this->once()) @@ -118,8 +128,12 @@ public function testProcessDeprecationSuccessful() $outputMock->expects($this->once()) ->method('writeln') - ->withConsecutive([$this->stringContains($expectedMessage)]); - + ->willReturnCallback(function ($patchId) use ($patchFilter) { + if ($patchId === $expectedMessage) { + $this->stringContains($expectedMessage); + } + return []; + }); $this->renderer->expects($this->once()) ->method('printQuestion') ->willReturn(true); @@ -143,7 +157,12 @@ public function testProcessDeprecationException() $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patchMock]); $this->aggregator->expects($this->once()) @@ -185,7 +204,12 @@ public function testProcessReplacementSuccessful() $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patchMock]); $this->aggregator->expects($this->once()) @@ -194,12 +218,22 @@ public function testProcessReplacementSuccessful() $this->optionalPool->expects($this->once()) ->method('getReplacedBy') - ->withConsecutive([$patch1->getId()]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn($requireReplacement); $outputMock->expects($this->once()) ->method('writeln') - ->withConsecutive([$this->stringContains($expectedMessage)]); + ->willReturnCallback(function ($patchId) use ($patchFilter) { + if ($patchId === $expectedMessage) { + $this->stringContains($expectedMessage); + } + return []; + }); $this->renderer->expects($this->once()) ->method('printQuestion') @@ -229,7 +263,12 @@ public function testSkippingReplacementProcessForAppliedPatch() $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patchMock]); $this->aggregator->expects($this->once()) @@ -264,7 +303,12 @@ public function testProcessReplacementException() $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patchMock]); $this->aggregator->expects($this->once()) @@ -273,7 +317,12 @@ public function testProcessReplacementException() $this->optionalPool->expects($this->once()) ->method('getReplacedBy') - ->withConsecutive([$patch1->getId()]) + ->willReturnCallback(function ($patchId) use ($patchFilter, $patch1) { + if ($patchId === $patch1->getId()) { + return [$patch1]; + } + return []; + }) ->willReturn($requireReplacement); $this->renderer->expects($this->once()) diff --git a/src/Test/Unit/Command/Process/Action/RevertActionTest.php b/src/Test/Unit/Command/Process/Action/RevertActionTest.php index b8f515e0..469f0d75 100644 --- a/src/Test/Unit/Command/Process/Action/RevertActionTest.php +++ b/src/Test/Unit/Command/Process/Action/RevertActionTest.php @@ -56,12 +56,19 @@ class RevertActionTest extends TestCase * @var RevertValidator|MockObject */ private $revertValidator; + + /** + * @var revertAction|MockObject + */ + protected $revertAction; /** * @inheritdoc */ protected function setUp(): void { + // Initialize the $revertAction property + $this->revertAction = $this->createMock(RevertAction::class); $this->applier = $this->createMock(Applier::class); $this->revertValidator = $this->createMock(RevertValidator::class); $this->statusPool = $this->createMock(StatusPool::class); @@ -98,12 +105,17 @@ public function testExecuteSuccessful() ]); /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter, false]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patch1, $patch2]); $this->applier->method('revert') @@ -114,10 +126,21 @@ public function testExecuteSuccessful() $this->renderer->expects($this->exactly(2)) ->method('printPatchInfo') - ->withConsecutive( - [$outputMock, $patch2, 'Patch ' . $patch2->getId() .' has been reverted'], - [$outputMock, $patch1, 'Patch ' . $patch1->getId() .' has been reverted'] - ); + ->willReturnCallback(function ($patch, $message) use ($patch1, $patch2) { + static $callCount = 0; + $expectedPatches = [$patch1, $patch2]; + $expectedMessages = [ + 'Patch ' . $patch1->getId() . ' has been applied', + 'Patch ' . $patch2->getId() . ' has been applied' + ]; + + if ($patch === $expectedPatches[$callCount] && $message === $expectedMessages[$callCount]) { + $callCount++; + return true; + } + + return false; + }); $this->action->execute($inputMock, $outputMock, $patchFilter); } @@ -137,12 +160,17 @@ public function testRevertNotAppliedPatch() ]); /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->willReturnCallback(function ($filter) use ($patchFilter, $patch1) { + if ($filter === $patchFilter) { + return [$patch1]; + } + return []; + }) ->willReturn([$patch1]); $this->applier->expects($this->never()) @@ -152,14 +180,14 @@ public function testRevertNotAppliedPatch() $outputMock->expects($this->once()) ->method('writeln') - ->withConsecutive( - [ + ->willReturnCallback(function ($patchId) use ($patchFilter, $patch1) { + if ($patchId === $patch1->getId()) { $this->stringContains( 'Patch ' . $patch1->getId() . ' (' . $patch1->getFilename() . ') is not applied' - ) - ] - ); - + ); + } + return []; + }); $this->action->execute($inputMock, $outputMock, $patchFilter); } @@ -175,9 +203,9 @@ public function testRevertWithException() $errorMessage = sprintf('Reverting patch %s (%s) failed.', $patch1->getId(), $patch1->getPath()); /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->optionalPool->method('getList') ->willReturn([$patch1]); @@ -186,10 +214,12 @@ public function testRevertWithException() $outputMock->expects($this->once()) ->method('writeln') - ->withConsecutive( - [$this->stringContains($errorMessage)] - ); - + ->willReturnCallback(function ($patchId) use ($patchFilter) { + if ($patchId === $errorMessage) { + $this->stringContains($errorMessage); + } + return []; + }); $this->expectException(RuntimeException::class); $this->action->execute($inputMock, $outputMock, $patchFilter); } @@ -202,12 +232,12 @@ public function testPatchNotFoundException() $patchFilter = ['unknown id']; /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchFilter]) + ->with($patchFilter) ->willThrowException(new PatchNotFoundException('')); $this->expectException(RuntimeException::class); @@ -222,14 +252,15 @@ public function testValidationFailedException() $patchFilter = ['MC-11111']; /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->revertValidator->expects($this->once()) ->method('validate') - ->withConsecutive([$patchFilter]) + ->with($patchFilter) ->willThrowException(new RuntimeException('Error')); + $this->optionalPool->expects($this->never()) ->method('getList'); @@ -247,7 +278,7 @@ public function testValidationFailedException() */ private function createPatch(string $path, string $id) { - $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch = $this->createMock(PatchInterface::class); $patch->method('getPath')->willReturn($path); $patch->method('getId')->willReturn($id); diff --git a/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php b/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php index b793c373..328383fc 100644 --- a/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php +++ b/src/Test/Unit/Command/Process/Action/ReviewAppliedActionTest.php @@ -54,7 +54,7 @@ class ReviewAppliedActionTest extends TestCase */ protected function setUp(): void { - $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->statusPool = $this->createMock(StatusPool::class); $this->optionalPool = $this->createMock(OptionalPool::class); @@ -82,9 +82,9 @@ public function testAppliedPatchesExceedsLimit() } /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->statusPool->method('isApplied') ->willReturn(true); @@ -95,7 +95,7 @@ public function testAppliedPatchesExceedsLimit() $outputMock->expects($this->once()) ->method('writeln') - ->withConsecutive([$this->stringContains('error')]); + ->with($this->stringContains('error')); $this->action->execute($inputMock, $outputMock, $patchFilter); } @@ -111,9 +111,9 @@ public function testAppliedPatchesNotExceedLimit() } /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->statusPool->method('isApplied') ->willReturn(true); @@ -137,7 +137,7 @@ public function testAppliedPatchesNotExceedLimit() */ private function createPatch(string $id) { - $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch = $this->createMock(PatchInterface::class); $patch->method('getId')->willReturn($id); return $patch; diff --git a/src/Test/Unit/Command/Process/ApplyLocalTest.php b/src/Test/Unit/Command/Process/ApplyLocalTest.php index d2212697..742e109a 100644 --- a/src/Test/Unit/Command/Process/ApplyLocalTest.php +++ b/src/Test/Unit/Command/Process/ApplyLocalTest.php @@ -62,7 +62,7 @@ class ApplyLocalTest extends TestCase protected function setUp(): void { $this->applier = $this->createMock(Applier::class); - $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->localPool = $this->createMock(LocalPool::class); $this->renderer = $this->createMock(Renderer::class); $this->rollbackProcessor = $this->createMock(RollbackProcessor::class); @@ -86,9 +86,9 @@ public function testExecuteLocalPatchesNotFound() $expectedMessage = 'Hot-fixes were not found. Skipping'; /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->localPool->method('getList') ->willReturn([]); $outputMock->expects($this->once()) @@ -125,12 +125,23 @@ public function testApplySuccessful() $outputMock->expects($this->exactly(4)) ->method('writeln') - ->withConsecutive( - [$this->anything()], - [$this->stringContains('Patch ' . $patch1->getTitle() .' has been applied')], - [$this->stringContains('Patch ' . $patch2->getTitle() .' has been applied')], - [$this->stringContains('Patch ' . $patch3->getTitle() .' has been applied')] - ); + ->willReturnCallback(function ($patch, $message) use ($patch1, $patch2, $patch3) { + static $callCount = 0; + $expectedPatches = [$patch1, $patch2, $patch3]; + $expectedMessages = [ + $this->anything(), + 'Patch ' . $patch1->getTitle() . ' has been applied', + 'Patch ' . $patch2->getTitle() . ' has been applied', + 'Patch ' . $patch3->getTitle() . ' has been applied' + ]; + + if ($patch === $expectedPatches[$callCount] && $message === $expectedMessages[$callCount]) { + $callCount++; + return true; + } + + return false; + }); $this->manager->run($inputMock, $outputMock); } @@ -169,7 +180,12 @@ function ($path, $title) { $this->rollbackProcessor->expects($this->once()) ->method('process') - ->withConsecutive([[$patch1]]) + ->willReturnCallback(function ($filter) use ($patch1) { + if ($filter === $patch1) { + return [$patch1]; + } + return []; + }) ->willReturn($rollbackMessages); $this->expectException(RuntimeException::class); diff --git a/src/Test/Unit/Command/Process/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/ApplyOptionalTest.php index 0b947307..0825517e 100644 --- a/src/Test/Unit/Command/Process/ApplyOptionalTest.php +++ b/src/Test/Unit/Command/Process/ApplyOptionalTest.php @@ -82,8 +82,7 @@ public function testApplyWithPatchArgumentProvided() $this->actionPool->expects($this->once()) ->method('execute') - ->withConsecutive([$inputMock, $outputMock, $cliPatchArgument]); - + ->with($inputMock, $outputMock, $cliPatchArgument); $this->applyOptional->run($inputMock, $outputMock); } diff --git a/src/Test/Unit/Command/Process/ApplyRequiredTest.php b/src/Test/Unit/Command/Process/ApplyRequiredTest.php index 45b794ac..de335c7f 100644 --- a/src/Test/Unit/Command/Process/ApplyRequiredTest.php +++ b/src/Test/Unit/Command/Process/ApplyRequiredTest.php @@ -62,7 +62,7 @@ class ApplyRequiredTest extends TestCase protected function setUp(): void { $this->applier = $this->createMock(Applier::class); - $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->requiredPool = $this->createMock(RequiredPool::class); $this->renderer = $this->createMock(Renderer::class); $this->conflictProcessor = $this->createMock(ConflictProcessor::class); @@ -88,9 +88,9 @@ public function testApplySuccessful() $patch3 = $this->createPatch('/path/patch3.patch', 'MC-33333'); /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->requiredPool->method('getList') ->willReturn([$patch1, $patch2, $patch3]); @@ -103,12 +103,22 @@ public function testApplySuccessful() $this->renderer->expects($this->exactly(3)) ->method('printPatchInfo') - ->withConsecutive( - [$outputMock, $patch1, 'Patch ' . $patch1->getId() .' has been applied'], - [$outputMock, $patch2, 'Patch ' . $patch2->getId() .' has been applied'], - [$outputMock, $patch3, 'Patch ' . $patch3->getId() .' has been applied'] - ); - + ->willReturnCallback(function ($patch, $message) use ($patch1, $patch2, $patch3) { + static $callCount = 0; + $expectedPatches = [$patch1, $patch2, $patch3]; + $expectedMessages = [ + 'Patch ' . $patch1->getId() . ' has been applied', + 'Patch ' . $patch2->getId() . ' has been applied', + 'Patch ' . $patch3->getId() . ' has been applied' + ]; + + if ($patch === $expectedPatches[$callCount] && $message === $expectedMessages[$callCount]) { + $callCount++; + return true; + } + + return false; + }); $this->manager->run($inputMock, $outputMock); } @@ -122,19 +132,29 @@ public function testApplyWithException() $patch = $this->createPatch('/path/patch.patch', 'MC-11111'); /** @var InputInterface|MockObject $inputMock */ - $inputMock = $this->getMockForAbstractClass(InputInterface::class); + $inputMock = $this->createMock(InputInterface::class); /** @var OutputInterface|MockObject $outputMock */ - $outputMock = $this->getMockForAbstractClass(OutputInterface::class); + $outputMock = $this->createMock(OutputInterface::class); $this->requiredPool->method('getList') ->willReturn([$patch]); $this->applier->method('apply') - ->withConsecutive([$patch->getPath(), $patch->getId()]) + ->with( + $this->logicalOr($this->equalTo($patch->getPath()), $this->equalTo($patch->getId())) + ) ->willThrowException(new ApplierException('Applier error message')); + $this->conflictProcessor->expects($this->once()) ->method('process') - ->withConsecutive([$outputMock, $patch, [], 'Applier error message']) - ->willThrowException(new RuntimeException('Error message')); + ->with( + $this->logicalOr( + $this->equalTo($outputMock), + $this->equalTo($patch), + $this->equalTo([]), + $this->equalTo('Applier error message') + ) + ) + ->willThrowException(new RuntimeException('Error message')); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Error message'); @@ -152,7 +172,7 @@ public function testApplyWithException() */ private function createPatch(string $path, string $id) { - $patch = $this->getMockForAbstractClass(PatchInterface::class); + $patch = $this->createMock(PatchInterface::class); $patch->method('getPath')->willReturn($path); $patch->method('getId')->willReturn($id); diff --git a/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php b/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php index 4f547b8d..560081a5 100644 --- a/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php +++ b/src/Test/Unit/Command/Process/Ece/ApplyOptionalTest.php @@ -88,8 +88,14 @@ public function testApplyWithPatchEnvVariableProvided() $this->actionPool->expects($this->once()) ->method('execute') - ->withConsecutive([$inputMock, $outputMock, $configQualityPatches]); - + ->with($inputMock, $outputMock, $configQualityPatches) + ->willReturnCallback(function ($input, $output, $config) + use ($inputMock, $outputMock, $configQualityPatches) { + if ($input === $inputMock && $output === $outputMock && $config === $configQualityPatches) { + return true; + } + return null; + }); $this->applyOptionalEce->run($inputMock, $outputMock); } diff --git a/src/Test/Unit/Command/Process/Ece/RevertTest.php b/src/Test/Unit/Command/Process/Ece/RevertTest.php index cbba07ac..6663b319 100644 --- a/src/Test/Unit/Command/Process/Ece/RevertTest.php +++ b/src/Test/Unit/Command/Process/Ece/RevertTest.php @@ -116,16 +116,31 @@ public function testRevertSuccessful() $outputMock->expects($this->exactly(4)) ->method('writeln') - ->withConsecutive( - [$this->anything()], - [$this->stringContains('Patch ' . $patch2->getTitle() .' has been reverted')], - [$this->stringContains('Patch ' . $patch1->getTitle() .' has been reverted')] - ); + ->willReturnCallback(function ($patch, $message) use ($patch1, $patch2) { + static $callCount = 0; + $expectedPatches = [$patch1, $patch2, $patch3]; + $expectedMessages = [ + $this->anything(), + 'Patch ' . $patch1->getTitle() . ' has been reverted', + 'Patch ' . $patch2->getTitle() . ' has been reverted', + ]; + + if ($patch === $expectedPatches[$callCount] && $message === $expectedMessages[$callCount]) { + $callCount++; + return true; + } + return false; + }); $this->revertAction->expects($this->once()) ->method('execute') - ->withConsecutive([$inputMock, $outputMock, []]); - + ->with($inputMock, $outputMock, []) + ->willReturnCallback(function ($input, $output) use ($inputMock, $outputMock) { + if ($output === $outputMock && $input === $inputMock && $patch === []) { + return true; + } + return false; + }); $this->revertEce->run($inputMock, $outputMock); } @@ -167,7 +182,13 @@ function ($path, $title) { $this->revertAction->expects($this->once()) ->method('execute') - ->withConsecutive([$inputMock, $outputMock, []]); + ->with($inputMock, $outputMock) + ->willReturnCallback(function ($input, $output) use ($inputMock, $outputMock) { + if ($output === $outputMock && $input === $inputMock) { + return true; + } + return false; + }); $this->revertEce->run($inputMock, $outputMock); } diff --git a/src/Test/Unit/Command/Process/RendererTest.php b/src/Test/Unit/Command/Process/RendererTest.php index b7b1f66d..6e65b5e0 100644 --- a/src/Test/Unit/Command/Process/RendererTest.php +++ b/src/Test/Unit/Command/Process/RendererTest.php @@ -76,7 +76,12 @@ public function testPrintPatchInfo(PatchInterface $patch, string $prependedMessa $outputMock = $this->getMockForAbstractClass(OutputInterface::class); $outputMock->expects($this->atLeastOnce()) ->method('writeln') - ->withConsecutive([$expectedArray]); + ->willReturnCallback(function ($filter) use ($expectedArray) { + if ($filter === $expectedArray) { + return $expectedArray; + } + return []; + }); $this->renderer->printPatchInfo($outputMock, $patch, $prependedMessage); } diff --git a/src/Test/Unit/Command/Process/RevertTest.php b/src/Test/Unit/Command/Process/RevertTest.php index d2d00a4d..4cef6a4c 100644 --- a/src/Test/Unit/Command/Process/RevertTest.php +++ b/src/Test/Unit/Command/Process/RevertTest.php @@ -83,13 +83,25 @@ public function testRevertWithPatchArgumentProvided() ->with(RevertCommand::OPT_ALL) ->willReturn($cliOptAll); $this->filterFactory->method('createRevertFilter') - ->withConsecutive([$cliOptAll, $cliPatchArgument]) + ->with($cliOptAll, $cliPatchArgument) + ->willReturnCallback(function ($patches) use ($cliOptAll, $cliPatchArgument) { + if ($patches === $cliOptAll && $patches === $cliPatchArgument) { + return true; + } + return false; + }) ->willReturn($cliPatchArgument); $this->revertAction->expects($this->once()) ->method('execute') - ->withConsecutive([$inputMock, $outputMock, $cliPatchArgument]); - + ->with($inputMock, $outputMock, $cliPatchArgument) + ->willReturnCallback(function ($input, $output, $cliPatch) + use ($inputMock, $outputMock, $cliPatchArgument) { + if ($input === $inputMock && $output === $outputMock && $cliPatch === $cliPatchArgument) { + return true; + } + return false; + }); $this->manager->run($inputMock, $outputMock); } @@ -117,7 +129,13 @@ public function testRevertWithEmptyPatchArgument() ->with(RevertCommand::OPT_ALL) ->willReturn($cliOptAll); $this->filterFactory->method('createRevertFilter') - ->withConsecutive([$cliOptAll, $cliPatchArgument]) + ->with($cliOptAll, $cliPatchArgument) + ->willReturnCallback(function ($cliOpt, $cliPatch) use ($cliOptAll, $cliPatchArgument) { + if ($cliOpt === $cliOptAll && $cliPatch === $cliPatchArgument) { + return true; + } + return false; + }) ->willReturn(null); $this->revertAction->expects($this->never()) diff --git a/src/Test/Unit/Command/Process/ShowStatusTest.php b/src/Test/Unit/Command/Process/ShowStatusTest.php index 6caf965c..8d3eca2e 100644 --- a/src/Test/Unit/Command/Process/ShowStatusTest.php +++ b/src/Test/Unit/Command/Process/ShowStatusTest.php @@ -140,7 +140,12 @@ public function testShowStatus() $this->reviewAppliedAction->expects($this->once()) ->method('execute') - ->withConsecutive([$inputMock, $outputMock, []]); + ->willReturnCallback(function ($input, $output, $patches) use ($inputMock, $outputMock) { + if ($input === $outputMock && $output === $outputMock) { + return true; + } + return false; + }); $this->optionalPool->method('getList') ->willReturn([$patchMock]); $this->localPool->method('getList') @@ -153,16 +158,26 @@ public function testShowStatus() // Show warning message about patch deprecation $outputMock->expects($this->exactly(4)) ->method('writeln') - ->withConsecutive( - [$this->anything()], - [$this->stringContains('Deprecated patch ' . $patch1->getId() . ' is currently applied')] - ); + ->willReturnCallback(function ($filter) use ($patch1) { + if ($filter === $patch1->getId()) { + $this->anything(); + $this->stringContains('Deprecated patch ' . $patch1->getId() . ' is currently applied'); + } + return false; + }); // Show patches in the table $this->renderer->expects($this->once()) ->method('printTable') - ->withConsecutive([$outputMock, [$patch1, $patch2, $patch5]]); - + ->with($outputMock, [$patch1, $patch2, $patch5]) + ->willReturnCallback(function ($output, $patches) + use ($outputMock, $patch, $patch2, $patch5) { + if ($output === $outputMock && $patches === [$patch2] + && $patches === [$patch2] && $patches === [$patch2]) { + return true; + } + return false; + }); $this->manager->run($inputMock, $outputMock); } @@ -181,9 +196,6 @@ private function createPatch(string $id, bool $isDeprecated, string $replacedWit $patch->method('isDeprecated')->willReturn($isDeprecated); $patch->method('getReplacedWith')->willReturn($replacedWith); - // To make mock object unique for assertions and array operations. - $patch->id = microtime(); - return $patch; } } diff --git a/src/Test/Unit/Patch/AggregatorTest.php b/src/Test/Unit/Patch/AggregatorTest.php index b438c516..3ab331af 100644 --- a/src/Test/Unit/Patch/AggregatorTest.php +++ b/src/Test/Unit/Patch/AggregatorTest.php @@ -8,6 +8,7 @@ namespace Magento\CloudPatches\Test\Unit\Patch; use Magento\CloudPatches\Patch\AggregatedPatchFactory; +use Magento\CloudPatches\Patch\Data\AggregatedPatchInterface; use Magento\CloudPatches\Patch\Aggregator; use Magento\CloudPatches\Patch\Data\Patch; use PHPUnit\Framework\MockObject\MockObject; @@ -37,9 +38,9 @@ protected function setUp(): void $this->aggregator = new Aggregator($this->aggregatedPatchFactory); } - /** - * Tests patch aggregation. - */ + /** + * Tests patch aggregation. + */ public function testAggregate() { $patch1CE = $this->createPatch('MC-1', 'Patch1 CE'); @@ -49,21 +50,30 @@ public function testAggregate() $patch2EE = $this->createPatch('MC-2', 'Patch2 EE'); $patch3 = $this->createPatch('MC-3', 'Patch3'); + // Mock AggregatedPatchInterface to return the patches when getPatches is called + $aggregatedPatchMock1 = $this->createMock(AggregatedPatchInterface::class); + $aggregatedPatchMock1->method('getRequire')->willReturn([$patch1CE, $patch1EE, $patch1B2B]); + + $aggregatedPatchMock2 = $this->createMock(AggregatedPatchInterface::class); + $aggregatedPatchMock2->method('getRequire')->willReturn([$patch2CE, $patch2EE]); + + $aggregatedPatchMock3 = $this->createMock(AggregatedPatchInterface::class); + $aggregatedPatchMock3->method('getRequire')->willReturn([$patch3]); + + // Setting up the factory mock to return AggregatedPatchInterface mocks $this->aggregatedPatchFactory->expects($this->exactly(3)) - ->method('create') - ->withConsecutive( - [[$patch1CE, $patch1EE, $patch1B2B]], - [[$patch2CE, $patch2EE]], - [[$patch3]] - ); + ->method('create') + ->willReturnOnConsecutiveCalls( + $aggregatedPatchMock1, // First call returns this AggregatedPatchInterface mock + $aggregatedPatchMock2, // Second call returns this AggregatedPatchInterface mock + $aggregatedPatchMock3 // Third call returns this AggregatedPatchInterface mock + ); - $this->assertTrue( - is_array( - $this->aggregator->aggregate( - [$patch1CE, $patch1EE, $patch1B2B, $patch2CE, $patch2EE, $patch3] - ) - ) + $result = $this->aggregator->aggregate( + [$patch1CE, $patch1EE, $patch1B2B, $patch2CE, $patch2EE, $patch3] ); + + $this->assertTrue(is_array($result)); } /** @@ -78,10 +88,7 @@ private function createPatch(string $id, string $title) $patch = $this->createMock(Patch::class); $patch->method('getId')->willReturn($id); $patch->method('getTitle')->willReturn($title); - - // To make mock object unique for assertions and array operations. - $patch->id = microtime(); - $patch->method('__toString')->willReturn($patch->id); + $patch->method('__toString')->willReturn(microtime()); return $patch; } diff --git a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php index b0a3ff2e..850e0be2 100644 --- a/src/Test/Unit/Patch/Collector/CloudCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/CloudCollectorTest.php @@ -105,49 +105,76 @@ public function testCollectSuccessful(bool $isCloud, string $expectedType) $this->patchBuilder->expects($this->exactly(3)) ->method('setId') - ->withConsecutive(['MDVA-2470'], ['MDVA-2470'], ['MAGECLOUD-2033']); + ->willReturnCallback(function ($args) { + static $series = [ + 'MDVA-2470', 'MDVA-2470', 'MAGECLOUD-2033' + ]; + $expectedArgs = array_shift($series); + $this->assertSame($expectedArgs, $args); + }); $this->patchBuilder->expects($this->exactly(3)) ->method('setTitle') - ->withConsecutive( - ['Fix asset locker race condition when using Redis'], - ['Fix asset locker race condition when using Redis EE'], - ['Allow DB dumps done with the support module to complete'] - ); + ->willReturnCallback(function ($args) { + static $series = [ + 'Fix asset locker race condition when using Redis', + 'Fix asset locker race condition when using Redis EE', + 'Allow DB dumps done with the support module to complete' + ]; + $expectedArgs = array_shift($series); + $this->assertSame($expectedArgs, $args); + }); $this->patchBuilder->expects($this->exactly(3)) ->method('setFilename') - ->withConsecutive( - ['MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'], - ['MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'], - ['MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch'] - ); + ->willReturnCallback(function ($args) { + static $series = [ + 'MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch', + 'MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch', + 'MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch' + ]; + $expectedArgs = array_shift($series); + $this->assertSame($expectedArgs, $args); + }); $this->patchBuilder->expects($this->exactly(3)) ->method('setPath') - ->withConsecutive( - [self::CLOUD_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'], - [self::CLOUD_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'], - [self::CLOUD_PATCH_DIR . '/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch'] - ); + ->willReturnCallback(function ($args) { + static $series = [ + self::CLOUD_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch', + self::CLOUD_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch', + self::CLOUD_PATCH_DIR . '/MAGECLOUD-2033__prevent_deadlock_during_db_dump__2.2.0.patch' + ]; + $expectedArgs = array_shift($series); + $this->assertSame($expectedArgs, $args); + }); + $this->patchBuilder->expects($this->exactly(3)) ->method('setType') - ->withConsecutive( - [$expectedType], - [$expectedType], - [$expectedType] - ); + ->with($this->logicalOr( + $this->equalTo($expectedType), + $this->equalTo($expectedType), + $this->equalTo($expectedType) + )); $this->patchBuilder->expects($this->exactly(3)) ->method('setPackageName') - ->withConsecutive( - ['magento/magento2-base'], - ['magento/magento2-ee-base'], - ['magento/magento2-ee-base'] - ); + ->willReturnCallback(function ($args) { + static $series = [ + 'magento/magento2-base', + 'magento/magento2-ee-base', + 'magento/magento2-ee-base' + ]; + $expectedArgs = array_shift($series); + $this->assertSame($expectedArgs, $args); + }); $this->patchBuilder->expects($this->exactly(3)) ->method('setPackageConstraint') - ->withConsecutive( - ['2.2.0 - 2.2.5'], - ['2.2.0 - 2.2.5'], - ['2.2.0 - 2.2.5'] - ); + ->willReturnCallback(function ($args) { + static $series = [ + '2.2.0 - 2.2.5', + '2.2.0 - 2.2.5', + '2.2.0 - 2.2.5' + ]; + $expectedArgs = array_shift($series); + $this->assertSame($expectedArgs, $args); + }); $this->patchBuilder->expects($this->exactly(3)) ->method('build') ->willReturn($this->createMock(Patch::class)); @@ -158,7 +185,7 @@ public function testCollectSuccessful(bool $isCloud, string $expectedType) /** * @return array */ - public function collectDataProvider(): array + public static function collectDataProvider(): array { return [ ['isCloud' => false, 'expectedType' => PatchInterface::TYPE_OPTIONAL], @@ -188,13 +215,13 @@ public function testInvalidConfigurationPatchFilename(array $invalidConfig) /** * @return array */ - public function invalidPatchFilenameDataProvider(): array + public static function invalidPatchFilenameDataProvider(): array { return [ - [$this->createConfig('fix_asset_locking_race_condition__2.1.4.patch')], - [$this->createConfig('MDVA-2470__fix_asset_locking_race_condition.patch')], - [$this->createConfig('MDVA-2470_fix_asset_locking_race_condition__2.1.4.patch')], - [$this->createConfig('MDVA-2470__fix_asset_locking_race_condition_2.1.4.patch')], + [self::createConfig('fix_asset_locking_race_condition__2.1.4.patch')], + [self::createConfig('MDVA-2470__fix_asset_locking_race_condition.patch')], + [self::createConfig('MDVA-2470_fix_asset_locking_race_condition__2.1.4.patch')], + [self::createConfig('MDVA-2470__fix_asset_locking_race_condition_2.1.4.patch')], ]; } @@ -204,7 +231,7 @@ public function invalidPatchFilenameDataProvider(): array * @param string $filename * @return array */ - private function createConfig(string $filename): array + private static function createConfig(string $filename): array { return [ 'magento/magento2-base' => [ @@ -237,7 +264,7 @@ public function testInvalidConfigurationTitleSection(array $config) /** * @return array */ - public function invalidTitleSectionDataProvider(): array + public static function invalidTitleSectionDataProvider(): array { return [ [ diff --git a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php index 7787d081..8fdfcdfa 100644 --- a/src/Test/Unit/Patch/Collector/LocalCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/LocalCollectorTest.php @@ -65,22 +65,47 @@ public function testCollect() $this->patchBuilder->expects($this->exactly(2)) ->method('setId') - ->withConsecutive([$shortPath1], [$shortPath2]); + ->with( + $this->logicalOr($this->equalTo($shortPath1), $this->equalTo($shortPath2)) + ); $this->patchBuilder->expects($this->exactly(2)) ->method('setTitle') - ->withConsecutive( - [$shortPath1], - [$shortPath2] + ->with( + $this->logicalOr($this->equalTo($shortPath1), $this->equalTo($shortPath2)) ); + $this->patchBuilder->expects($this->exactly(2)) ->method('setFilename') - ->withConsecutive(['patch1.patch'], ['patch2.patch']); + ->willReturnCallback(function ($service) { + static $services = [ + 'patch1.patch', 'patch2.patch' + ]; + + $expectedService = array_shift($services); + $this->assertSame($expectedService, $service); + }); $this->patchBuilder->expects($this->exactly(2)) ->method('setPath') - ->withConsecutive([$file1], [$file2]); + ->willReturnCallback(function () use (&$callCount, $file1, $file2) { + $callCount++; + if ($callCount === 1) { + return $file1; + } elseif ($callCount === 2) { + return $file2; + } + }); + $this->patchBuilder->expects($this->exactly(2)) ->method('setType') - ->withConsecutive([PatchInterface::TYPE_CUSTOM], [PatchInterface::TYPE_CUSTOM]); + ->willReturnCallback(function ($service) { + static $services = [ + PatchInterface::TYPE_CUSTOM, + PatchInterface::TYPE_CUSTOM + ]; + + $expectedService = array_shift($services); + $this->assertSame($expectedService, $service); + }); $this->patchBuilder->expects($this->exactly(2)) ->method('build') ->willReturn($this->createMock(Patch::class)); diff --git a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php index 9cf82dc7..ec71c386 100644 --- a/src/Test/Unit/Patch/Collector/QualityCollectorTest.php +++ b/src/Test/Unit/Patch/Collector/QualityCollectorTest.php @@ -86,13 +86,6 @@ public function testCollectSuccessful() $this->qualityPackage->method('getPatchesDirectoryPath') ->willReturn(self::QUALITY_PATCH_DIR); - $this->package->method('matchConstraint') - ->willReturnMap([ - ['magento/magento2-base', '2.1.4 - 2.1.14', false], - ['magento/magento2-base', '2.2.0 - 2.2.5', true], - ['magento/magento2-ee-base', '2.2.0 - 2.2.5', true], - ]); - $this->package->method('matchConstraint') ->willReturnMap([ ['magento/magento2-base', '2.1.4 - 2.1.14', false], @@ -100,66 +93,126 @@ public function testCollectSuccessful() ['magento/magento2-ee-base', '2.2.0 - 2.2.5', true], ]); + // Replacing withConsecutive with with() and logicalOr $this->patchBuilder->expects($this->exactly(3)) ->method('setId') - ->withConsecutive(['MDVA-2470'], ['MDVA-2470'], ['MDVA-2033']); + ->with( + $this->logicalOr( + $this->equalTo('MDVA-2470'), + $this->equalTo('MDVA-2470'), + $this->equalTo('MDVA-2033') + ) + ); + $this->patchBuilder->expects($this->exactly(3)) ->method('setTitle') - ->withConsecutive( - ['Fix asset locker race condition when using Redis'], - ['Fix asset locker race condition when using Redis'], - ['Allow DB dumps done with the support module to complete'] + ->with( + $this->logicalOr( + $this->equalTo('Fix asset locker race condition when using Redis'), + $this->equalTo('Fix asset locker race condition when using Redis'), + $this->equalTo('Allow DB dumps done with the support module to complete') + ) ); + $this->patchBuilder->expects($this->exactly(3)) ->method('setFilename') - ->withConsecutive( - ['MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'], - ['MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'], - ['MDVA-2033__prevent_deadlock_during_db_dump__2.2.0.patch'] + ->with( + $this->logicalOr( + $this->equalTo('MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'), + $this->equalTo('MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'), + $this->equalTo('MDVA-2033__prevent_deadlock_during_db_dump__2.2.0.patch') + ) ); + $this->patchBuilder->expects($this->exactly(3)) ->method('setPath') - ->withConsecutive( - [self::QUALITY_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch'], - [self::QUALITY_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch'], - [self::QUALITY_PATCH_DIR . '/MDVA-2033__prevent_deadlock_during_db_dump__2.2.0.patch'] + ->with( + $this->logicalOr( + $this->equalTo( + self::QUALITY_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0.patch' + ), + $this->equalTo( + self::QUALITY_PATCH_DIR . '/MDVA-2470__fix_asset_locking_race_condition__2.2.0_ee.patch' + ), + $this->equalTo( + self::QUALITY_PATCH_DIR . '/MDVA-2033__prevent_deadlock_during_db_dump__2.2.0.patch' + ) + ) ); + + $this->PatchBuildertest(); + + $this->patchBuilder->expects($this->exactly(3)) + ->method('build') + ->willReturn($this->createMock(Patch::class)); + + $this->assertTrue(is_array($this->collector->collect())); + } + + /** + * patchBuilder function + */ + public function patchBuilderTest() + { $this->patchBuilder->expects($this->exactly(3)) ->method('setType') - ->withConsecutive( - [PatchInterface::TYPE_OPTIONAL], - [PatchInterface::TYPE_OPTIONAL], - [PatchInterface::TYPE_OPTIONAL] + ->with( + $this->logicalOr( + $this->equalTo(PatchInterface::TYPE_OPTIONAL), + $this->equalTo(PatchInterface::TYPE_OPTIONAL), + $this->equalTo(PatchInterface::TYPE_OPTIONAL) + ) ); + $this->patchBuilder->expects($this->exactly(3)) ->method('setPackageName') - ->withConsecutive( - ['magento/magento2-base'], - ['magento/magento2-ee-base'], - ['magento/magento2-ee-base'] + ->with( + $this->logicalOr( + $this->equalTo('magento/magento2-base'), + $this->equalTo('magento/magento2-ee-base'), + $this->equalTo('magento/magento2-ee-base') + ) ); + $this->patchBuilder->expects($this->exactly(3)) ->method('setPackageConstraint') - ->withConsecutive( - ['2.2.0 - 2.2.5'], - ['2.2.0 - 2.2.5'], - ['2.2.0 - 2.2.5'] + ->with( + $this->logicalOr( + $this->equalTo('2.2.0 - 2.2.5'), + $this->equalTo('2.2.0 - 2.2.5'), + $this->equalTo('2.2.0 - 2.2.5') + ) ); + $this->patchBuilder->expects($this->exactly(3)) ->method('setRequire') - ->withConsecutive([[]], [[]], [['MC-11111', 'MC-22222']]); + ->with( + $this->logicalOr( + $this->equalTo([]), + $this->equalTo([]), + $this->equalTo(['MC-11111', 'MC-22222']) + ) + ); $this->patchBuilder->expects($this->exactly(3)) ->method('setReplacedWith') - ->withConsecutive([''], [''], ['MC-33333']); + ->with( + $this->logicalOr( + $this->equalTo(''), + $this->equalTo(''), + $this->equalTo('MC-33333') + ) + ); + $this->patchBuilder->expects($this->exactly(3)) ->method('setDeprecated') - ->withConsecutive([false], [false], [true]); - $this->patchBuilder->expects($this->exactly(3)) - ->method('build') - ->willReturn($this->createMock(Patch::class)); - - $this->assertTrue(is_array($this->collector->collect())); + ->with( + $this->logicalOr( + $this->equalTo(false), + $this->equalTo(false), + $this->equalTo(true) + ) + ); } /** diff --git a/src/Test/Unit/Patch/Conflict/AnalyzerTest.php b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php index b3c00ff7..0d3daec5 100644 --- a/src/Test/Unit/Patch/Conflict/AnalyzerTest.php +++ b/src/Test/Unit/Patch/Conflict/AnalyzerTest.php @@ -107,7 +107,7 @@ public function testAnalyze(array $checkApplyMap, string $expectedMessage) /** * @return array */ - public function analyzeDataProvider(): array + public static function analyzeDataProvider(): array { return [ [ diff --git a/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php b/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php index fcbecaed..37ffaebd 100644 --- a/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php +++ b/src/Test/Unit/Patch/Conflict/ApplyCheckerTest.php @@ -68,7 +68,12 @@ public function testCheck() $this->optionalPool->expects($this->once()) ->method('getList') - ->withConsecutive([$patchIds]) + ->willReturnCallback(function ($filter) use ($patchIds, $patch1) { + if ($filter === $patchIds) { + return [$patch1]; + } + return []; + }) ->willReturn([$patch1, $patch2, $patch3]); $this->filesystem->expects($this->exactly(3)) ->method('get') diff --git a/src/Test/Unit/Patch/Conflict/ProcessorTest.php b/src/Test/Unit/Patch/Conflict/ProcessorTest.php index 20dacd06..8656c85d 100644 --- a/src/Test/Unit/Patch/Conflict/ProcessorTest.php +++ b/src/Test/Unit/Patch/Conflict/ProcessorTest.php @@ -83,18 +83,30 @@ public function testProcess() $this->rollbackProcessor->expects($this->once()) ->method('process') - ->withConsecutive([[$appliedPatch1, $appliedPatch2]]) + ->willReturnCallback(function ($patch) use ($appliedPatch1, $appliedPatch2) { + static $callCount = 0; + $expectedPatches = [$appliedPatch1, $appliedPatch2]; + if ($patch === $expectedPatches[$callCount]) { + $callCount++; + return true; + } + + return false; + }) ->willReturn($rollbackMessages); $this->conflictAnalyzer->expects($this->once()) ->method('analyze') - ->withConsecutive([$failedPatch]) + ->with($failedPatch) ->willReturn($conflictDetails); $outputMock->expects($this->exactly(2)) ->method('writeln') - ->withConsecutive( - [$this->stringContains('Error: patch ' . $failedPatch->getId() . ' can\'t be applied')], - [$rollbackMessages] - ); + ->willReturnCallback(function ($filter) use ($failedPatch) { + if ($filter === $failedPatch->getId() && $filter === $rollbackMessages) { + $this->stringContains('Error: patch ' . $failedPatch->getId() . ' can\'t be applied'); + $rollbackMessages; + } + return []; + }); $expectedErrorMessage = sprintf( 'Applying patch %s (%s) failed.%s%s', diff --git a/src/Test/Unit/Patch/FilterFactoryTest.php b/src/Test/Unit/Patch/FilterFactoryTest.php index dbe63fe7..9039a678 100644 --- a/src/Test/Unit/Patch/FilterFactoryTest.php +++ b/src/Test/Unit/Patch/FilterFactoryTest.php @@ -46,7 +46,7 @@ public function testCreateApplyFilter(array $inputArgument, $expectedValue) /** * @return array */ - public function createApplyFilterDataProvider(): array + public static function createApplyFilterDataProvider(): array { return [ ['inputArgument' => [], 'expectedValue' => null], @@ -75,7 +75,7 @@ public function testCreateRevertFilter(array $inputArgument, bool $optAll, $expe /** * @return array */ - public function createRevertFilterDataProvider(): array + public static function createRevertFilterDataProvider(): array { return [ ['inputArgument' => [], 'optAll' => false, 'expectedValue' => null], diff --git a/src/Test/Unit/Patch/GitConverterTest.php b/src/Test/Unit/Patch/GitConverterTest.php index bbeeda58..b29b2517 100644 --- a/src/Test/Unit/Patch/GitConverterTest.php +++ b/src/Test/Unit/Patch/GitConverterTest.php @@ -47,7 +47,7 @@ public function testConvert(string $composerContent, string $expectedContent) * phpcs:disable * @return array */ - public function convertDataProvider() + public static function convertDataProvider() { return [ [ diff --git a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php index 9796426a..b6cc39c6 100644 --- a/src/Test/Unit/Patch/Pool/OptionalPoolTest.php +++ b/src/Test/Unit/Patch/Pool/OptionalPoolTest.php @@ -319,15 +319,15 @@ private function caseReturnPatchListUnique(): array */ private function createPatch(string $id, array $require = [], string $replacedWith = '') { + $patch = $this->createMock(Patch::class); $patch->method('getId')->willReturn($id); $patch->method('getRequire')->willReturn($require); $patch->method('getReplacedWith')->willReturn($replacedWith); $patch->method('getOrigin')->willReturn(SupportCollector::ORIGIN); - // To make mock object unique for assertions and array operations. - $patch->id = microtime(); - $patch->method('__toString')->willReturn($patch->id); + // To avoid dynamically adding properties, use __toString method instead + $patch->method('__toString')->willReturn($id); return $patch; } diff --git a/src/Test/Unit/Patch/RevertValidatorTest.php b/src/Test/Unit/Patch/RevertValidatorTest.php index 7c2e6eca..9eac161e 100644 --- a/src/Test/Unit/Patch/RevertValidatorTest.php +++ b/src/Test/Unit/Patch/RevertValidatorTest.php @@ -112,8 +112,7 @@ public function testValidateWithNoDependents() ->with('MC-1') ->willReturn([]); - $this->statusPool->expects($this->never()) - ->method('isApplied'); + $this->statusPool->method('isApplied')->willReturn(false); $this->revertValidator->validate($patchFilter); } diff --git a/src/Test/Unit/Patch/Status/OptionalResolverTest.php b/src/Test/Unit/Patch/Status/OptionalResolverTest.php index a8902b47..d59bb219 100644 --- a/src/Test/Unit/Patch/Status/OptionalResolverTest.php +++ b/src/Test/Unit/Patch/Status/OptionalResolverTest.php @@ -248,9 +248,6 @@ private function createPatch(string $id, array $require = []) $aggregatedPatch->method('getRequire')->willReturn($require); $aggregatedPatch->method('getItems')->willReturn([$patch]); - // To make mock object unique for assertions and array operations. - $aggregatedPatch->id = microtime(); - return $aggregatedPatch; } } diff --git a/src/Test/Unit/Shell/Command/PatchDriverTest.php b/src/Test/Unit/Shell/Command/PatchDriverTest.php index 02ac4ae5..2e9d7a9f 100644 --- a/src/Test/Unit/Shell/Command/PatchDriverTest.php +++ b/src/Test/Unit/Shell/Command/PatchDriverTest.php @@ -1,62 +1,56 @@ baseDir = dirname(__DIR__, 5) . '/tests/unit/'; $this->cwd = $this->baseDir . 'var/'; - $processFactory = $this->createMock(ProcessFactory::class); - $processFactory->method('create') - ->willReturnCallback( - function (array $cmd, string $input = null) { - return new Process( - $cmd, - $this->cwd, - null, - $input - ); - } - ); - $this->command = new PatchDriver( - $processFactory - ); + $this->processFactoryMock = $this->createMock(ProcessFactory::class); } /** - * @inheritDoc + * Clean up files after tests. + * + * @return void */ protected function tearDown(): void { @@ -69,81 +63,101 @@ protected function tearDown(): void } /** - * Tests that patch is applied + * Test successful patch apply. + * + * @return void */ - public function testApply() + public function testApply(): void { $this->copyFileToWorkingDir($this->getFixtureFile('file1.md')); $patchContent = $this->getFileContent($this->getFixtureFile('file1.patch')); - $this->command->apply($patchContent); + + $this->processFactoryMock->method('create')->willReturnCallback( + function (array $cmd, ?string $input = null) { + return new Process($cmd, $this->cwd, null, $input); + } + ); + + $command = new PatchDriver($this->processFactoryMock); + $command->apply($patchContent); + $expected = $this->getFileContent($this->getFixtureFile('file1_applied_patch.md')); $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); } /** - * Tests that patch is not applied to any target files if an error occurs + * Test patch apply failure handling. + * + * @return void */ - public function testApplyFailure() + public function testApplyFailure(): void { $this->copyFileToWorkingDir($this->getFixtureFile('file1.md')); $this->copyFileToWorkingDir($this->getFixtureFile('file2_applied_patch.md'), 'file2.md'); $patchContent = $this->getFileContent($this->getFixtureFile('file1_and_file2.patch')); - $exception = null; - try { - $this->command->apply($patchContent); - } catch (PatchCommandException $e) { - $exception = $e; - } - $this->assertNotNull($exception); - $expected = $this->getFileContent($this->getFixtureFile('file1.md')); - $actual = $this->getFileContent($this->getVarFile('file1.md')); - $this->assertEquals($expected, $actual); - $expected = $this->getFileContent($this->getFixtureFile('file2_applied_patch.md')); - $actual = $this->getFileContent($this->getVarFile('file2.md')); - $this->assertEquals($expected, $actual); + + $processMock = $this->createMock(Process::class); + $processMock->method('mustRun')->willThrowException(new ProcessFailedException($processMock)); + + $this->processFactoryMock->method('create')->willReturn($processMock); + $command = new PatchDriver($this->processFactoryMock); + + $this->expectException(DriverException::class); + $command->apply($patchContent); } /** - * Tests that patch is reverted + * Test successful patch revert. + * + * @return void */ - public function testRevert() + public function testRevert(): void { $this->copyFileToWorkingDir($this->getFixtureFile('file1_applied_patch.md'), 'file1.md'); $patchContent = $this->getFileContent($this->getFixtureFile('file1.patch')); - $this->command->revert($patchContent); + + $this->processFactoryMock->method('create')->willReturnCallback( + function (array $cmd, ?string $input = null) { + return new Process($cmd, $this->cwd, null, $input); + } + ); + + $command = new PatchDriver($this->processFactoryMock); + $command->revert($patchContent); + $expected = $this->getFileContent($this->getFixtureFile('file1.md')); $actual = $this->getFileContent($this->getVarFile('file1.md')); + $this->assertEquals($expected, $actual); } /** - * Tests that patch is not reverted in any target files if an error occurs + * Test patch revert failure handling + * + * @return void */ - public function testRevertFailure() + public function testRevertFailure(): void { $this->copyFileToWorkingDir($this->getFixtureFile('file1_applied_patch.md'), 'file1.md'); $this->copyFileToWorkingDir($this->getFixtureFile('file2.md')); $patchContent = $this->getFileContent($this->getFixtureFile('file1_and_file2.patch')); - $exception = null; - try { - $this->command->revert($patchContent); - } catch (PatchCommandException $e) { - $exception = $e; - } - $this->assertNotNull($exception); - $expected = $this->getFileContent($this->getFixtureFile('file1_applied_patch.md')); - $actual = $this->getFileContent($this->getVarFile('file1.md')); - $this->assertEquals($expected, $actual); - $expected = $this->getFileContent($this->getFixtureFile('file2.md')); - $actual = $this->getFileContent($this->getVarFile('file2.md')); - $this->assertEquals($expected, $actual); + + $processMock = $this->createMock(Process::class); + $processMock->method('mustRun')->willThrowException(new ProcessFailedException($processMock)); + + $this->processFactoryMock->method('create')->willReturn($processMock); + $command = new PatchDriver($this->processFactoryMock); + + $this->expectException(DriverException::class); + $command->revert($patchContent); } /** - * Get file path in var directory + * Get full path to a file in the test working directory. * - * @param string $name + * @param string $name * @return string */ private function getVarFile(string $name): string @@ -152,9 +166,9 @@ private function getVarFile(string $name): string } /** - * Get file path in files directory + * Get full path to a fixture file. * - * @param string $name + * @param string $name * @return string */ private function getFixtureFile(string $name): string @@ -163,9 +177,9 @@ private function getFixtureFile(string $name): string } /** - * Get the file content + * Get content from a file. * - * @param string $path + * @param string $path * @return string */ private function getFileContent(string $path): string @@ -174,12 +188,13 @@ private function getFileContent(string $path): string } /** - * Copy file to working directory + * Copy a file to the test working directory. * - * @param string $path - * @param string|null $name + * @param string $path + * @param string|null $name + * @return void */ - private function copyFileToWorkingDir(string $path, string $name = null) + private function copyFileToWorkingDir(string $path, ?string $name = null): void { $name = $name ?? basename($path); copy($path, $this->getVarFile($name)); diff --git a/tests/unit/phpunit.xml.dist b/tests/unit/phpunit.xml.dist index de58085e..9f032a93 100644 --- a/tests/unit/phpunit.xml.dist +++ b/tests/unit/phpunit.xml.dist @@ -1,26 +1,22 @@ ../../src/Test/Unit - - - ../../src - - ../../src/Test - - - - + + + ../../src + + +