Skip to content

Commit

Permalink
Merge branch 'magento-commerce:2.4-develop' into application-server
Browse files Browse the repository at this point in the history
  • Loading branch information
andimov authored Apr 24, 2023
2 parents 2812732 + 9c0e123 commit 6a3b613
Show file tree
Hide file tree
Showing 20 changed files with 1,272 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
<waitForPageLoad stepKey="waitForTaxRateLoad"/>

<!-- delete the rule -->
<waitForElementVisible selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="waitForDelete"/>
<click stepKey="clickDelete" selector="{{AdminStoresMainActionsSection.deleteButton}}"/>
<waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmationModal"/>
<click stepKey="clickOk" selector="{{AdminConfirmationModalSection.ok}}"/>
<see stepKey="seeSuccess" selector="{{AdminMessagesSection.success}}" userInput="deleted"/>
<waitForText stepKey="seeSuccess" selector="{{AdminMessagesSection.success}}" userInput="deleted"/>
</actionGroup>
</actionGroups>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\CmsGraphQl\Model\Resolver\Page;

use Magento\Cms\Api\Data\PageInterface;
use Magento\Cms\Model\Page;
use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface;

/**
* Identity for resolved CMS page for resolver cache type
*/
class ResolverCacheIdentity implements IdentityInterface
{
/**
* @var string
*/
private $cacheTag = Page::CACHE_TAG;

/**
* Get page ID from resolved data
*
* @param array $resolvedData
* @return string[]
*/
public function getIdentities(array $resolvedData): array
{
return empty($resolvedData[PageInterface::PAGE_ID]) ?
[] : [sprintf('%s_%s', $this->cacheTag, $resolvedData[PageInterface::PAGE_ID])];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\CmsGraphQl\Test\Integration\Model\Resolver;

use Magento\Cms\Api\Data\PageInterface;
use Magento\Cms\Model\PageRepository;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\App\Cache\StateInterface as CacheStateInterface;
use Magento\Framework\App\Cache\Type\FrontendPool;
use Magento\Framework\ObjectManagerInterface;
use Magento\GraphQl\Service\GraphQlRequest;
use Magento\GraphQlCache\Model\Cache\Query\Resolver\Result\Type as GraphQlResolverCache;
use Magento\GraphQlCache\Model\Plugin\Query\Resolver\Result\Cache as ResolverResultCachePlugin;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

/**
* Test GraphQl Resolver cache saves and loads properly
* @magentoAppArea graphql
*/
class PageTest extends TestCase
{
/**
* @var ObjectManagerInterface
*/
private $objectManager;

/**
* @var GraphQlRequest
*/
private $graphQlRequest;

/**
* @var ResolverResultCachePlugin
*/
private $originalResolverResultCachePlugin;

/**
* @var SearchCriteriaBuilder
*/
private $searchCriteriaBuilder;

/**
* @var PageRepository
*/
private $pageRepository;

/**
* @var CacheStateInterface
*/
private $cacheState;

/**
* @var bool
*/
private $originalCacheStateEnabledStatus;

protected function setUp(): void
{
$this->objectManager = $objectManager = Bootstrap::getObjectManager();
$this->graphQlRequest = $objectManager->create(GraphQlRequest::class);
$this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
$this->pageRepository = $objectManager->get(PageRepository::class);
$this->originalResolverResultCachePlugin = $objectManager->get(ResolverResultCachePlugin::class);

$this->cacheState = $objectManager->get(CacheStateInterface::class);
$this->originalCacheStateEnabledStatus = $this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER);
$this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, true);
}

protected function tearDown(): void
{
$objectManager = $this->objectManager;

// reset to original resolver plugin
$objectManager->addSharedInstance($this->originalResolverResultCachePlugin, ResolverResultCachePlugin::class);

// clean graphql resolver cache and reset to original enablement status
$objectManager->get(GraphQlResolverCache::class)->clean();
$this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, $this->originalCacheStateEnabledStatus);
}

/**
* Test that result can be loaded continuously after saving once when passing the same arguments
*
* @magentoDataFixture Magento/Cms/Fixtures/page_list.php
* @return void
*/
public function testResultIsLoadedMultipleTimesAfterOnlyBeingSavedOnce()
{
$objectManager = $this->objectManager;
$page = $this->getPageByTitle('Page with 1column layout');

$frontendPool = $objectManager->get(FrontendPool::class);

$cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
->enableProxyingToOriginalMethods()
->setConstructorArgs([
$frontendPool
])
->getMock();

// assert cache proxy calls load at least once for the same CMS page query
$cacheProxy
->expects($this->atLeastOnce())
->method('load');

// assert save is called at most once for the same CMS page query
$cacheProxy
->expects($this->once())
->method('save');

$resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
'graphQlResolverCache' => $cacheProxy,
]);

// override resolver plugin with plugin instance containing cache proxy class
$objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);

$query = $this->getQuery($page->getIdentifier());

// send request and assert save is called
$this->graphQlRequest->send($query);

// send again and assert save is not called (i.e. result is loaded from resolver cache)
$this->graphQlRequest->send($query);

// send again with whitespace appended and assert save is not called (i.e. result is loaded from resolver cache)
$this->graphQlRequest->send($query . ' ');

// send again with a different field and assert save is not called (i.e. result is loaded from resolver cache)
$differentQuery = $this->getQuery($page->getIdentifier(), ['meta_title']);
$this->graphQlRequest->send($differentQuery);
}

/**
* Test that resolver plugin does not call GraphQlResolverCache's save or load methods when it is disabled
*
* @magentoDataFixture Magento/Cms/Fixtures/page_list.php
* @return void
*/
public function testNeitherSaveNorLoadAreCalledWhenResolverCacheIsDisabled()
{
$objectManager = $this->objectManager;
$page = $this->getPageByTitle('Page with 1column layout');

// disable graphql resolver cache
$this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, false);

$frontendPool = $objectManager->get(FrontendPool::class);

$cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
->enableProxyingToOriginalMethods()
->setConstructorArgs([
$frontendPool
])
->getMock();

// assert cache proxy never calls load
$cacheProxy
->expects($this->never())
->method('load');

// assert save is also never called
$cacheProxy
->expects($this->never())
->method('save');

$resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
'graphQlResolverCache' => $cacheProxy,
]);

// override resolver plugin with plugin instance containing cache proxy class
$objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);

$query = $this->getQuery($page->getIdentifier());

// send request multiple times and assert neither save nor load are called
$this->graphQlRequest->send($query);
$this->graphQlRequest->send($query);
}

public function testSaveIsNeverCalledWhenMissingRequiredArgumentInQuery()
{
$objectManager = $this->objectManager;

$frontendPool = $objectManager->get(FrontendPool::class);

$cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
->enableProxyingToOriginalMethods()
->setConstructorArgs([
$frontendPool
])
->getMock();

// assert cache proxy never calls save
$cacheProxy
->expects($this->never())
->method('save');

$resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
'graphQlResolverCache' => $cacheProxy,
]);

// override resolver plugin with plugin instance containing cache proxy class
$objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);

$query = <<<QUERY
{
cmsPage {
title
}
}
QUERY;

// send request multiple times and assert save is never called
$this->graphQlRequest->send($query);
$this->graphQlRequest->send($query);
}

private function getQuery(string $identifier, array $fields = ['title']): string
{
$fields = implode(PHP_EOL, $fields);

return <<<QUERY
{
cmsPage(identifier: "$identifier") {
$fields
}
}
QUERY;
}

private function getPageByTitle(string $title): PageInterface
{
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('title', $title)
->create();

$pages = $this->pageRepository->getList($searchCriteria)->getItems();

/** @var PageInterface $page */
$page = reset($pages);

return $page;
}
}
18 changes: 18 additions & 0 deletions app/code/Magento/CmsGraphQl/etc/di.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\GraphQlCache\Model\Cache\Query\Resolver\Result\TagResolver">
<arguments>
<argument name="invalidatableObjectTypes" xsi:type="array">
<item name="Magento\Cms\Api\Data\PageInterface" xsi:type="string">
Magento\Cms\Api\Data\PageInterface
</item>
</argument>
</arguments>
</type>
</config>
9 changes: 9 additions & 0 deletions app/code/Magento/CmsGraphQl/etc/graphql/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,13 @@
</argument>
</arguments>
</type>
<type name="Magento\GraphQlCache\Model\Cache\Query\Resolver\Result\ResolverIdentityClassLocator">
<arguments>
<argument name="cacheableResolverClassNameIdentityMap" xsi:type="array">
<item name="Magento\CmsGraphQl\Model\Resolver\Page" xsi:type="string">
Magento\CmsGraphQl\Model\Resolver\Page\ResolverCacheIdentity
</item>
</argument>
</arguments>
</type>
</config>
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\GraphQlCache\Model\Cache\Query\Resolver\Result;

use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\GraphQlCache\Model\Resolver\IdentityPool;

class ResolverIdentityClassLocator
{
/**
* @var IdentityPool
*/
private $identityPool;

/**
* Map of Resolver Class Name => Identity Provider
*
* @var string[]
*/
private array $cacheableResolverClassNameIdentityMap;

/**
* @param IdentityPool $identityPool
* @param array $cacheableResolverClassNameIdentityMap
*/
public function __construct(
IdentityPool $identityPool,
array $cacheableResolverClassNameIdentityMap
) {
$this->identityPool = $identityPool;
$this->cacheableResolverClassNameIdentityMap = $cacheableResolverClassNameIdentityMap;
}

/**
* Get Identity provider based on $resolver
*
* @param ResolverInterface $resolver
* @return IdentityInterface|null
*/
public function getIdentityFromResolver(ResolverInterface $resolver): ?IdentityInterface
{
$matchingIdentityProviderClassName = null;

foreach ($this->cacheableResolverClassNameIdentityMap as $resolverClassName => $identityProviderClassName) {
if ($resolver instanceof $resolverClassName) {
$matchingIdentityProviderClassName = $identityProviderClassName;
break;
}
}

if (!$matchingIdentityProviderClassName) {
return null;
}

return $this->identityPool->get($matchingIdentityProviderClassName);
}
}
Loading

0 comments on commit 6a3b613

Please sign in to comment.