diff --git a/app/code/Magento/Persistent/Model/QuoteResourceWrapper.php b/app/code/Magento/Persistent/Model/QuoteResourceWrapper.php new file mode 100644 index 0000000000000..7cb51bc2db068 --- /dev/null +++ b/app/code/Magento/Persistent/Model/QuoteResourceWrapper.php @@ -0,0 +1,69 @@ +resourceConnection = $resourceConnection; + } + + /** + * Check if quote is active. + * + * @param int|null $quoteId + * @return bool + */ + public function isActive(?int $quoteId): bool + { + if (empty($quoteId)) { + return false; + } + $table = $this->resourceConnection->getTableName('quote'); + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from($table, 'is_active') + ->where('entity_id = ?', $quoteId); + + return (bool)$connection->fetchOne($select); + } + + /** + * Check if quote is persistent. + * + * @param int|null $quoteId + * @return bool + */ + public function isPersistent(?int $quoteId): bool + { + if (empty($quoteId)) { + return false; + } + $table = $this->resourceConnection->getTableName('quote'); + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from($table, 'is_persistent') + ->where('entity_id = ?', $quoteId); + + return (bool)$connection->fetchOne($select); + } +} diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index cf3d92fe985fc..894f372b29781 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -1,15 +1,19 @@ _persistentSession = $persistentSession; $this->quoteManager = $quoteManager; @@ -109,18 +112,17 @@ public function __construct( $this->_eventManager = $eventManager; $this->_persistentData = $persistentData; $this->request = $request; - $this->quoteRepository = $quoteRepository; + $this->quoteResourceWrapper = $quoteResourceWrapper ?: ObjectManager::getInstance() + ->get(QuoteResourceWrapper::class); } /** * Check and clear session data if persistent session expired * - * @param \Magento\Framework\Event\Observer $observer + * @param Observer $observer * @return void - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { if (!$this->_persistentData->canProcess($observer)) { return; @@ -141,7 +143,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->_checkoutSession->getQuoteId() && // persistent session does not expire on onepage checkout page !$this->isRequestFromCheckoutPage($this->request) && - $this->getQuote()->getIsPersistent() + (bool)$this->quoteResourceWrapper->isPersistent($this->_checkoutSession->getQuoteId()) ) { $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); @@ -153,58 +155,26 @@ public function execute(\Magento\Framework\Event\Observer $observer) * Checks if current quote marked as persistent and Persistence Functionality is disabled. * * @return bool - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function isPersistentQuoteOutdated(): bool { if (!($this->_persistentData->isEnabled() && $this->_persistentData->isShoppingCartPersist()) && !$this->_customerSession->isLoggedIn() && $this->_checkoutSession->getQuoteId() - && $this->isActiveQuote() + && $this->quoteResourceWrapper->isActive($this->_checkoutSession->getQuoteId()) ) { - return (bool)$this->getQuote()->getIsPersistent(); + return (bool)$this->quoteResourceWrapper->isPersistent($this->_checkoutSession->getQuoteId()); } return false; } - /** - * Getter for Quote with micro optimization - * - * @return Quote - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - private function getQuote(): Quote - { - if ($this->quote === null) { - $this->quote = $this->_checkoutSession->getQuote(); - } - return $this->quote; - } - - /** - * Check if quote is active. - * - * @return bool - */ - private function isActiveQuote(): bool - { - try { - $this->quoteRepository->getActive($this->_checkoutSession->getQuoteId()); - return true; - } catch (NoSuchEntityException $e) { - return false; - } - } - /** * Check current request is coming from onepage checkout page. * - * @param \Magento\Framework\App\RequestInterface $request + * @param RequestInterface $request * @return bool */ - private function isRequestFromCheckoutPage(\Magento\Framework\App\RequestInterface $request): bool + private function isRequestFromCheckoutPage(RequestInterface $request): bool { $requestUri = (string)$request->getRequestUri(); $refererUri = (string)$request->getServer('HTTP_REFERER'); diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteResourceWrapperTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteResourceWrapperTest.php new file mode 100644 index 0000000000000..9ee1f86615b9a --- /dev/null +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteResourceWrapperTest.php @@ -0,0 +1,222 @@ +resourceConnectionMock = $this->createMock(ResourceConnection::class); + $this->connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); + $this->selectMock = $this->createMock(Select::class); + + $this->model = new QuoteResourceWrapper($this->resourceConnectionMock); + + $this->resourceConnectionMock->method('getConnection') + ->willReturn($this->connectionMock); + $this->connectionMock->method('select') + ->willReturn($this->selectMock); + } + + /** + * Test isActive with null quote ID + */ + public function testIsActiveWithNullQuoteId(): void + { + $this->assertFalse($this->model->isActive(null)); + } + + /** + * Test isActive with active quote + */ + public function testIsActiveWithActiveQuote(): void + { + $quoteId = 123; + $tableName = 'quote'; + + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName') + ->with('quote') + ->willReturn($tableName); + + $this->selectMock->expects($this->once()) + ->method('from') + ->with($tableName, 'is_active') + ->willReturnSelf(); + + $this->selectMock->expects($this->once()) + ->method('where') + ->with('entity_id = ?', $quoteId) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($this->selectMock) + ->willReturn('1'); + + $this->assertTrue($this->model->isActive($quoteId)); + } + + /** + * Test isActive with inactive quote + */ + public function testIsActiveWithInactiveQuote(): void + { + $quoteId = 123; + $tableName = 'quote'; + + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName') + ->with('quote') + ->willReturn($tableName); + + $this->selectMock->expects($this->once()) + ->method('from') + ->with($tableName, 'is_active') + ->willReturnSelf(); + + $this->selectMock->expects($this->once()) + ->method('where') + ->with('entity_id = ?', $quoteId) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($this->selectMock) + ->willReturn('0'); + + $this->assertFalse($this->model->isActive($quoteId)); + } + + /** + * Test isPersistent with null quote ID + */ + public function testIsPersistentWithNullQuoteId(): void + { + $this->assertFalse($this->model->isPersistent(null)); + } + + /** + * Test isPersistent with persistent quote + */ + public function testIsPersistentWithPersistentQuote(): void + { + $quoteId = 123; + $tableName = 'quote'; + + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName') + ->with('quote') + ->willReturn($tableName); + + $this->selectMock->expects($this->once()) + ->method('from') + ->with($tableName, 'is_persistent') + ->willReturnSelf(); + + $this->selectMock->expects($this->once()) + ->method('where') + ->with('entity_id = ?', $quoteId) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($this->selectMock) + ->willReturn('1'); + + $this->assertTrue($this->model->isPersistent($quoteId)); + } + + /** + * Test isPersistent with non-persistent quote + */ + public function testIsPersistentWithNonPersistentQuote(): void + { + $quoteId = 123; + $tableName = 'quote'; + + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName') + ->with('quote') + ->willReturn($tableName); + + $this->selectMock->expects($this->once()) + ->method('from') + ->with($tableName, 'is_persistent') + ->willReturnSelf(); + + $this->selectMock->expects($this->once()) + ->method('where') + ->with('entity_id = ?', $quoteId) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($this->selectMock) + ->willReturn('0'); + + $this->assertFalse($this->model->isPersistent($quoteId)); + } + + /** + * Test type casting from database on isPersistent + */ + public function testIsPersistentTypeCasting(): void + { + $quoteId = 123; + $tableName = 'quote'; + + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName') + ->willReturn($tableName); + + $this->selectMock->expects($this->once()) + ->method('from') + ->willReturnSelf(); + + $this->selectMock->expects($this->once()) + ->method('where') + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->willReturn(''); + + $this->assertFalse($this->model->isPersistent($quoteId)); + } +} diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php index c26dde2b03ce9..c66dfbb920b70 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php @@ -1,7 +1,7 @@ disableOriginalConstructor() ->addMethods(['getRequestUri', 'getServer']) ->getMockForAbstractClass(); - $this->quoteRepositoryMock = $this->getMockForAbstractClass(CartRepositoryInterface::class); + $this->quoteResourceWrapperMock = $this->createMock(QuoteResourceWrapper::class); $this->model = new CheckExpirePersistentQuoteObserver( $this->sessionMock, @@ -110,13 +103,8 @@ protected function setUp(): void $this->customerSessionMock, $this->checkoutSessionMock, $this->requestMock, - $this->quoteRepositoryMock + $this->quoteResourceWrapperMock ); - $this->quoteMock = $this->getMockBuilder(Quote::class) - ->addMethods(['getIsPersistent']) - ->onlyMethods(['getCustomerIsGuest']) - ->disableOriginalConstructor() - ->getMock(); } public function testExecuteWhenCanNotApplyPersistentData() @@ -132,20 +120,30 @@ public function testExecuteWhenCanNotApplyPersistentData() public function testExecuteWhenPersistentIsNotEnabled() { - $quoteId = 'quote_id_1'; + $quoteId = 10; $this->persistentHelperMock ->expects($this->once()) ->method('canProcess') ->with($this->observerMock) ->willReturn(true); - $this->persistentHelperMock->expects($this->exactly(2))->method('isEnabled')->willReturn(false); - $this->checkoutSessionMock->expects($this->exactly(2))->method('getQuoteId')->willReturn($quoteId); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') + $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(false); + $this->checkoutSessionMock->expects($this->any())->method('getQuoteId')->willReturn($quoteId); + + $this->quoteResourceWrapperMock->expects($this->once()) + ->method('isActive') + ->with($quoteId) + ->willReturn(true); + $this->quoteResourceWrapperMock->expects($this->once()) + ->method('isPersistent') ->with($quoteId) - ->willThrowException(new NoSuchEntityException()); - $this->eventManagerMock->expects($this->never())->method('dispatch'); + ->willReturn(true); + + $this->eventManagerMock->expects($this->once())->method('dispatch'); + $this->quoteManagerMock->expects($this->once())->method('expire'); + $this->checkoutSessionMock->expects($this->once())->method('clearQuote'); + $this->customerSessionMock->expects($this->once())->method('setCustomerId')->with(null)->willReturnSelf(); + $this->model->execute($this->observerMock); } @@ -167,6 +165,8 @@ public function testExecuteWhenPersistentIsEnabled( InvokedCount $dispatchCounter, InvokedCount $setCustomerIdCounter ): void { + $quoteId = 10; + $this->persistentHelperMock ->expects($this->once()) ->method('canProcess') @@ -179,19 +179,21 @@ public function testExecuteWhenPersistentIsEnabled( ->method('isShoppingCartPersist') ->willReturn(true); $this->sessionMock->expects($this->atLeastOnce())->method('isPersistent')->willReturn(false); - $this->checkoutSessionMock - ->method('getQuote') - ->willReturn($this->quoteMock); - $this->quoteMock->method('getCustomerIsGuest')->willReturn(true); - $this->quoteMock->method('getIsPersistent')->willReturn(true); $this->customerSessionMock ->expects($this->atLeastOnce()) ->method('isLoggedIn') ->willReturn(false); $this->checkoutSessionMock - ->expects($this->atLeastOnce()) + ->expects($this->any()) ->method('getQuoteId') - ->willReturn(10); + ->willReturn($quoteId); + + // Mock the QuoteResourceWrapper calls + $this->quoteResourceWrapperMock->expects($this->any()) + ->method('isPersistent') + ->with($quoteId) + ->willReturn(true); + $this->eventManagerMock->expects($dispatchCounter)->method('dispatch'); $this->quoteManagerMock->expects($expireCounter)->method('expire'); $this->customerSessionMock