diff --git a/app/code/Magento/SalesRule/Model/Plugin/FrontController.php b/app/code/Magento/SalesRule/Model/Plugin/FrontController.php new file mode 100644 index 0000000000000..bf01769630284 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Plugin/FrontController.php @@ -0,0 +1,37 @@ +getMethod()); + $isReadOnly = ($method === 'GET'); + + $this->requestTypeRegistry->setIsGetRequestOrQuery($isReadOnly); + } +} diff --git a/app/code/Magento/SalesRule/Model/Plugin/GraphQlController.php b/app/code/Magento/SalesRule/Model/Plugin/GraphQlController.php new file mode 100644 index 0000000000000..7c98b73d7ee88 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Plugin/GraphQlController.php @@ -0,0 +1,38 @@ +requestTypeRegistry->reset(); + return $result; + } +} diff --git a/app/code/Magento/SalesRule/Model/Plugin/QueryParser.php b/app/code/Magento/SalesRule/Model/Plugin/QueryParser.php new file mode 100644 index 0000000000000..48d3e519055a0 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Plugin/QueryParser.php @@ -0,0 +1,47 @@ +definitions as $definition) { + if ($definition instanceof \GraphQL\Language\AST\OperationDefinitionNode) { + $operation = $definition; + break; + } + } + + if ($operation) { + $isOperationTypeQuery = ($operation->operation === 'query'); + $this->requestTypeRegistry->setIsGetRequestOrQuery($isOperationTypeQuery); + } + + return $documentNode; + } +} diff --git a/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php b/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php index 764658b3ac00a..9b96cee5b43b2 100644 --- a/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php +++ b/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php @@ -1,12 +1,16 @@ ruleResource = $ruleResource; } @@ -40,8 +61,24 @@ public function __construct(RuleResource $ruleResource) */ public function afterGetProductAttributes(Config $subject, array $attributeKeys): array { - if ($this->activeAttributeCodes === null) { - $this->activeAttributeCodes = array_column($this->ruleResource->getActiveAttributes(), 'attribute_code'); + if ($this->requestTypeRegistry->isGetRequestOrQuery()) { + return $attributeKeys; + } + + $cachedData = $this->cache->load(self::CACHE_KEY); + + if ($cachedData !== false) { + $this->activeAttributeCodes = $this->serializer->unserialize($cachedData); + } else { + $this->activeAttributeCodes = array_column( + $this->ruleResource->getActiveAttributes(), + 'attribute_code' + ); + $this->cache->save( + $this->serializer->serialize($this->activeAttributeCodes), + self::CACHE_KEY, + [self::CACHE_TAG] + ); } return array_merge($attributeKeys, $this->activeAttributeCodes); diff --git a/app/code/Magento/SalesRule/Model/Plugin/QuoteItemCollection.php b/app/code/Magento/SalesRule/Model/Plugin/QuoteItemCollection.php new file mode 100644 index 0000000000000..be5720d9d57de --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Plugin/QuoteItemCollection.php @@ -0,0 +1,36 @@ +getTriggerRecollect() == 1 && $this->requestTypeRegistry->isGetRequestOrQuery()) { + $this->requestTypeRegistry->setIsGetRequestOrQuery(false); + } + } +} diff --git a/app/code/Magento/SalesRule/Model/Plugin/RequestTypeRegistry.php b/app/code/Magento/SalesRule/Model/Plugin/RequestTypeRegistry.php new file mode 100644 index 0000000000000..d536deed37d86 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Plugin/RequestTypeRegistry.php @@ -0,0 +1,53 @@ +isGetRequestOrQuery = $state; + } + + /** + * Check if request is a get or query + * + * @return bool + */ + public function isGetRequestOrQuery(): bool + { + return $this->isGetRequestOrQuery; + } + + /** + * Resets the state of the object by setting the isGetRequestOrQuery property to false. + * + * @return void + */ + public function reset(): void + { + $this->isGetRequestOrQuery = false; + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/FrontControllerTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/FrontControllerTest.php new file mode 100644 index 0000000000000..d15dd20b4c707 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/FrontControllerTest.php @@ -0,0 +1,81 @@ +requestTypeRegistry = $this->createMock(RequestTypeRegistry::class); + $this->plugin = new FrontController($this->requestTypeRegistry); + $this->request = $this->getMockBuilder(RequestInterface::class) + ->addMethods(['getMethod']) + ->getMockForAbstractClass(); + $this->subject = $this->createMock(FrontControllerInterface::class); + } + + public function testBeforeDispatchSetsTrueForGetRequests(): void + { + $this->request->method('getMethod')->willReturn('GET'); + + $this->requestTypeRegistry + ->expects($this->once()) + ->method('setIsGetRequestOrQuery') + ->with(true); + + $this->plugin->beforeDispatch($this->subject, $this->request); + } + + public function testBeforeDispatchSetsFalseForPostRequests(): void + { + $this->request->method('getMethod')->willReturn('POST'); + + $this->requestTypeRegistry + ->expects($this->once()) + ->method('setIsGetRequestOrQuery') + ->with(false); + + $this->plugin->beforeDispatch($this->subject, $this->request); + } + + public function testBeforeDispatchIsCaseInsensitive(): void + { + + $this->request->method('getMethod')->willReturn('get'); // lowercase + + $this->requestTypeRegistry + ->expects($this->once()) + ->method('setIsGetRequestOrQuery') + ->with(true); + + $this->plugin->beforeDispatch($this->subject, $this->request); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/GraphQlControllerTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/GraphQlControllerTest.php new file mode 100644 index 0000000000000..9bf94fbe15805 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/GraphQlControllerTest.php @@ -0,0 +1,68 @@ +requestTypeRegistry = $this->createMock(RequestTypeRegistry::class); + $this->graphQlController = new GraphQlController($this->requestTypeRegistry); + $this->frontController = $this->createMock(FrontControllerInterface::class); + $this->response = $this->createMock(ResponseInterface::class); + } + + /** + * Test afterDispatch calls reset on RequestTypeRegistry + */ + public function testAfterDispatchResetsRequestTypeRegistry(): void + { + // Expect reset to be called on the RequestTypeRegistry + $this->requestTypeRegistry->expects($this->once()) + ->method('reset'); + + // Call the afterDispatch method + $result = $this->graphQlController->afterDispatch($this->frontController, $this->response); + + // Verify the response is returned unchanged + $this->assertSame($this->response, $result); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QueryParserTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QueryParserTest.php new file mode 100644 index 0000000000000..277a0cccf4677 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QueryParserTest.php @@ -0,0 +1,147 @@ +requestTypeRegistry = $this->createMock(RequestTypeRegistry::class); + $this->queryParser = new QueryParser($this->requestTypeRegistry); + $this->subject = $this->createMock(FrameworkQueryParser::class); + $this->documentNode = $this->createMock(DocumentNode::class); + } + + /** + * Test afterParse method with query operation + */ + public function testAfterParseWithQueryOperation(): void + { + // Create a mock operation definition node with 'query' operation + $operationNode = $this->createMock(OperationDefinitionNode::class); + $operationNode->operation = 'query'; + + // Create a mock definitions list with the operation node + $definitions = new NodeList([$operationNode]); + $this->documentNode->definitions = $definitions; + + // Expect RequestTypeRegistry to be called with true + $this->requestTypeRegistry->expects($this->once()) + ->method('setIsGetRequestOrQuery') + ->with(true); + + // Call the method + $result = $this->queryParser->afterParse($this->subject, $this->documentNode); + + // Assert the document node is returned unchanged + $this->assertSame($this->documentNode, $result); + } + + /** + * Test afterParse method with mutation operation + */ + public function testAfterParseWithMutationOperation(): void + { + // Create a mock operation definition node with 'mutation' operation + $operationNode = $this->createMock(OperationDefinitionNode::class); + $operationNode->operation = 'mutation'; + + // Create a mock definitions list with the operation node + $definitions = new NodeList([$operationNode]); + $this->documentNode->definitions = $definitions; + + // Expect RequestTypeRegistry to be called with false + $this->requestTypeRegistry->expects($this->once()) + ->method('setIsGetRequestOrQuery') + ->with(false); + + // Call the method + $result = $this->queryParser->afterParse($this->subject, $this->documentNode); + + // Assert the document node is returned unchanged + $this->assertSame($this->documentNode, $result); + } + + /** + * Test afterParse method with no operation definitions + */ + public function testAfterParseWithNoOperationDefinitions(): void + { + // Create an empty definitions list + $definitions = new NodeList([]); + $this->documentNode->definitions = $definitions; + + // Expect RequestTypeRegistry not to be called + $this->requestTypeRegistry->expects($this->never()) + ->method('setIsGetRequestOrQuery'); + + // Call the method + $result = $this->queryParser->afterParse($this->subject, $this->documentNode); + + // Assert the document node is returned unchanged + $this->assertSame($this->documentNode, $result); + } + + /** + * Test afterParse method with non-operation definition nodes + */ + public function testAfterParseWithNonOperationDefinitions(): void + { + // Create a mock node that is not an OperationDefinitionNode + $nonOperationNode = $this->createMock(\GraphQL\Language\AST\Node::class); + + // Create a mock definitions list with the non-operation node + $definitions = new NodeList([$nonOperationNode]); + $this->documentNode->definitions = $definitions; + + // Expect RequestTypeRegistry not to be called + $this->requestTypeRegistry->expects($this->never()) + ->method('setIsGetRequestOrQuery'); + + // Call the method + $result = $this->queryParser->afterParse($this->subject, $this->documentNode); + + // Assert the document node is returned unchanged + $this->assertSame($this->documentNode, $result); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteConfigProductAttributesTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteConfigProductAttributesTest.php index a06584cec2273..c855d678d622d 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteConfigProductAttributesTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteConfigProductAttributesTest.php @@ -1,15 +1,17 @@ ruleResource = $this->createMock(Rule::class); + $this->requestTypeRegistry = $this->createMock(RequestTypeRegistry::class); + $this->cache = $this->createMock(CacheInterface::class); + $this->serializer = $this->createMock(SerializerInterface::class); + $this->subject = $this->createMock(Config::class); - $this->plugin = $objectManager->getObject( - QuoteConfigProductAttributes::class, - [ - 'ruleResource' => $this->ruleResource - ] + $this->plugin = new QuoteConfigProductAttributes( + $this->ruleResource, + $this->requestTypeRegistry, + $this->cache, + $this->serializer ); } - public function testAfterGetProductAttributes() + public function testAfterGetProductAttributesWithCache() + { + $attributeCode = 'code of the attribute'; + $expected = [0 => $attributeCode]; + $serializedData = '["' . $attributeCode . '"]'; + + $this->requestTypeRegistry->expects($this->once()) + ->method('isGetRequestOrQuery') + ->willReturn(false); + + $this->cache->expects($this->once()) + ->method('load') + ->with('salesrule_active_product_attributes') + ->willReturn($serializedData); + + $this->serializer->expects($this->once()) + ->method('unserialize') + ->with($serializedData) + ->willReturn([$attributeCode]); + + $this->ruleResource->expects($this->never()) + ->method('getActiveAttributes'); + + $this->assertEquals($expected, $this->plugin->afterGetProductAttributes($this->subject, [])); + } + + public function testAfterGetProductAttributesWithoutCache() { - $subject = $this->createMock(Config::class); $attributeCode = 'code of the attribute'; $expected = [0 => $attributeCode]; + $serializedData = '["' . $attributeCode . '"]'; + + $this->requestTypeRegistry->expects($this->once()) + ->method('isGetRequestOrQuery') + ->willReturn(false); + + $this->cache->expects($this->once()) + ->method('load') + ->with('salesrule_active_product_attributes') + ->willReturn(false); $this->ruleResource->expects($this->once()) ->method('getActiveAttributes') @@ -53,6 +113,24 @@ public function testAfterGetProductAttributes() ] ); - $this->assertEquals($expected, $this->plugin->afterGetProductAttributes($subject, [])); + $this->serializer->expects($this->once()) + ->method('serialize') + ->with([$attributeCode]) + ->willReturn($serializedData); + + $this->cache->expects($this->once()) + ->method('save') + ->with($serializedData, 'salesrule_active_product_attributes', ['salesrule']); + + $this->assertEquals($expected, $this->plugin->afterGetProductAttributes($this->subject, [])); + } + + public function testAfterGetProductAttributesRequestTypePostOrMutation() + { + $this->requestTypeRegistry->expects($this->once()) + ->method('isGetRequestOrQuery') + ->willReturn(true); + + $this->assertEquals([], $this->plugin->afterGetProductAttributes($this->subject, [])); } } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteItemCollectionTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteItemCollectionTest.php new file mode 100644 index 0000000000000..8b9ff3a39ddf7 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteItemCollectionTest.php @@ -0,0 +1,122 @@ +requestTypeRegistry = $this->createMock(RequestTypeRegistry::class); + $this->plugin = new QuoteItemCollection($this->requestTypeRegistry); + $this->quoteItemCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->addMethods(['getTriggerRecollect']) + ->getMock(); + } + + /** + * Test beforeSetQuote when triggerRecollect is 1 and request is GET/Query + */ + public function testBeforeSetQuoteWithTriggerRecollectAndGetRequest(): void + { + // Set up the quote mock to return 1 for getTriggerRecollect + $this->quote->expects($this->once()) + ->method('getTriggerRecollect') + ->willReturn(1); + + // Set up the request type registry to indicate this is a GET request + $this->requestTypeRegistry->expects($this->once()) + ->method('isGetRequestOrQuery') + ->willReturn(true); + + // Expect setIsGetRequestOrQuery to be called with false + $this->requestTypeRegistry->expects($this->once()) + ->method('setIsGetRequestOrQuery') + ->with(false); + + // Execute the method + $this->plugin->beforeSetQuote($this->quoteItemCollection, $this->quote); + } + + /** + * Test beforeSetQuote when triggerRecollect is 0 and request is GET/Query + */ + public function testBeforeSetQuoteWithNoTriggerRecollectAndGetRequest(): void + { + // Set up the quote mock to return 0 for getTriggerRecollect + $this->quote->expects($this->once()) + ->method('getTriggerRecollect') + ->willReturn(0); + + // Expect setIsGetRequestOrQuery NOT to be called + $this->requestTypeRegistry->expects($this->never()) + ->method('setIsGetRequestOrQuery'); + + // Execute the method + $this->plugin->beforeSetQuote($this->quoteItemCollection, $this->quote); + } + + /** + * Test beforeSetQuote when triggerRecollect is 1 but request is not GET/Query + */ + public function testBeforeSetQuoteWithTriggerRecollectAndNonGetRequest(): void + { + // Set up the quote mock to return 1 for getTriggerRecollect + $this->quote->expects($this->once()) + ->method('getTriggerRecollect') + ->willReturn(1); + + // Set up the request type registry to indicate this is NOT a GET request + $this->requestTypeRegistry->expects($this->once()) + ->method('isGetRequestOrQuery') + ->willReturn(false); + + // Expect setIsGetRequestOrQuery NOT to be called + $this->requestTypeRegistry->expects($this->never()) + ->method('setIsGetRequestOrQuery'); + + // Execute the method + $this->plugin->beforeSetQuote($this->quoteItemCollection, $this->quote); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/RequestTypeRegistryTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/RequestTypeRegistryTest.php new file mode 100644 index 0000000000000..3dcd6b16ad626 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/RequestTypeRegistryTest.php @@ -0,0 +1,56 @@ +state = new RequestTypeRegistry(); + } + + public function testDefaultStateIsFalse() + { + $this->assertFalse($this->state->isGetRequestOrQuery()); + } + + public function testIsGetRequestOrQueryTrue() + { + $this->state->setIsGetRequestOrQuery(true); + $this->assertTrue($this->state->isGetRequestOrQuery()); + } + + public function testIsGetRequestOrQueryFalse() + { + $this->state->setIsGetRequestOrQuery(true); + $this->state->setIsGetRequestOrQuery(false); + $this->assertFalse($this->state->isGetRequestOrQuery()); + } + + public function testResetAndMultipleToggle() + { + $this->assertFalse($this->state->isGetRequestOrQuery()); + + $this->state->setIsGetRequestOrQuery(true); + $this->assertTrue($this->state->isGetRequestOrQuery()); + + $this->state->reset(); + $this->assertFalse($this->state->isGetRequestOrQuery()); + + $this->state->setIsGetRequestOrQuery(true); + $this->assertTrue($this->state->isGetRequestOrQuery()); + } +} diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index cd841b36338e4..b3aa5b68191d3 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -54,6 +54,11 @@ + + + diff --git a/app/code/Magento/SalesRule/etc/frontend/di.xml b/app/code/Magento/SalesRule/etc/frontend/di.xml index 2f5251e27f021..256f3ca51401f 100644 --- a/app/code/Magento/SalesRule/etc/frontend/di.xml +++ b/app/code/Magento/SalesRule/etc/frontend/di.xml @@ -1,7 +1,7 @@ @@ -32,4 +32,9 @@ + + + diff --git a/app/code/Magento/SalesRule/etc/graphql/di.xml b/app/code/Magento/SalesRule/etc/graphql/di.xml new file mode 100644 index 0000000000000..c844dbf6d64c6 --- /dev/null +++ b/app/code/Magento/SalesRule/etc/graphql/di.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/app/code/Magento/SalesRule/etc/webapi_rest/di.xml b/app/code/Magento/SalesRule/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..377063359a35a --- /dev/null +++ b/app/code/Magento/SalesRule/etc/webapi_rest/di.xml @@ -0,0 +1,15 @@ + + + + + + +