Files
2026-09-15 10:26:34 +08:00

396 lines
13 KiB
PHP

<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\GraphQl\Eav;
use Magento\Eav\Model\AttributeRepository;
use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract;
use Magento\Store\Model\StoreRepository;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException;
/**
* Test caching for custom attribute metadata GraphQL query.
*/
class CustomAttributesMetadataCacheTest extends GraphQLPageCacheAbstract
{
/**
* @var \Magento\Framework\ObjectManagerInterface
*/
private $objectManager;
/**
* @inheritdoc
*/
public function setUp(): void
{
$this->objectManager = Bootstrap::getObjectManager();
parent::setUp();
}
/**
* @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php
* @magentoConfigFixture default/system/full_page_cache/caching_application 2
*
* @return void
*/
public function testCacheHitMiss()
{
$query = $this->getAttributeQuery("dropdown_attribute", "catalog_product");
$response = $this->assertCacheMissAndReturnResponse($query, []);
$this->assertResponseFields(
$response['body']['customAttributeMetadata']['items'][0],
[
'attribute_code' => 'dropdown_attribute',
'attribute_type' => 'String',
'entity_type' => 'catalog_product',
'input_type' => 'select',
]
);
$response = $this->assertCacheHitAndReturnResponse($query, []);
$this->assertResponseFields(
$response['body']['customAttributeMetadata']['items'][0],
[
'attribute_code' => 'dropdown_attribute',
'attribute_type' => 'String',
'entity_type' => 'catalog_product',
'input_type' => 'select',
]
);
}
/**
* @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php
* @magentoApiDataFixture Magento/Store/_files/store.php
* @magentoConfigFixture default/system/full_page_cache/caching_application 2
*
* @return void
*/
public function testCacheDifferentStores()
{
$query = $this->getAttributeQuery("dropdown_attribute", "catalog_product");
/** @var AttributeRepository $eavAttributeRepo */
$eavAttributeRepo = $this->objectManager->get(AttributeRepository::class);
/** @var StoreRepository $storeRepo */
$storeRepo = $this->objectManager->get(StoreRepository::class);
$stores = $storeRepo->getList();
$attribute = $eavAttributeRepo->get("catalog_product", "dropdown_attribute");
$options = $attribute->getOptions();
//prepare unique option values per-store
$storeOptions = [];
foreach ($options as $option) {
$optionValues = $option->getData();
if (!empty($optionValues['value'])) {
$storeOptions['value'][$optionValues['value']] = [];
foreach ($stores as $store) {
$storeOptions['value'][$optionValues['value']][$store->getId()] = $store->getCode()
. '_'
. $optionValues['label'];
}
}
}
//save attribute with new option values
$attribute->addData(['option' => $storeOptions]);
$eavAttributeRepo->save($attribute);
// get attribute metadata for test store and assert it missed the cache
$response = $this->assertCacheMissAndReturnResponse($query, ['Store' => 'test']);
$options = $response['body']['customAttributeMetadata']['items'][0]['attribute_options'];
$this->assertOptionValuesPerStore($storeOptions, 'test', $stores, $options);
// get attribute metadata for test store again and assert it has hit the cache
$response = $this->assertCacheHitAndReturnResponse($query, ['Store' => 'test']);
$options = $response['body']['customAttributeMetadata']['items'][0]['attribute_options'];
$this->assertOptionValuesPerStore($storeOptions, 'test', $stores, $options);
$response = $this->assertCacheMissAndReturnResponse($query, ['Store' => 'default']);
$options = $response['body']['customAttributeMetadata']['items'][0]['attribute_options'];
$this->assertOptionValuesPerStore($storeOptions, 'default', $stores, $options);
$response = $this->assertCacheHitAndReturnResponse($query, ['Store' => 'default']);
$options = $response['body']['customAttributeMetadata']['items'][0]['attribute_options'];
$this->assertOptionValuesPerStore($storeOptions, 'default', $stores, $options);
}
/**
* Assert attribute option labels for each store provided.
*
* @param array $storeOptions
* @param string $storeCode
* @param \Magento\Store\Api\Data\StoreInterface[] $stores
* @param array $options
*
* @return void
*/
private function assertOptionValuesPerStore($storeOptions, $storeCode, $stores, $options)
{
foreach ($options as $option) {
$this->assertEquals(
$storeOptions['value'][$option['value']][$stores[$storeCode]->getId()],
$option['label']
);
}
}
/**
* @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php
* @magentoConfigFixture default/system/full_page_cache/caching_application 2
*
* @return void
*/
public function testCacheInvalidation()
{
$query = $this->getAttributeQuery("dropdown_attribute", "catalog_product");
// check cache missed on first query
$response = $this->assertCacheMissAndReturnResponse($query, []);
$this->assertResponseFields(
$response['body']['customAttributeMetadata']['items'][0],
[
'attribute_code' => 'dropdown_attribute',
'attribute_type' => 'String',
'entity_type' => 'catalog_product',
'input_type' => 'select',
]
);
// assert cache hit on second query
$this->assertCacheHitAndReturnResponse($query, []);
/** @var AttributeRepository $eavAttributeRepo */
$eavAttributeRepo = $this->objectManager->get(AttributeRepository::class);
$attribute = $eavAttributeRepo->get("catalog_product", "dropdown_attribute");
$attribute->setIsRequired(1);
$eavAttributeRepo->save($attribute);
// assert cache miss after changes
$this->assertCacheMissAndReturnResponse($query, []);
// assert cache hits on second query after changes
$response = $this->assertCacheHitAndReturnResponse($query, []);
$this->assertResponseFields(
$response['body']['customAttributeMetadata']['items'][0],
[
'attribute_code' => 'dropdown_attribute',
'attribute_type' => 'String',
'entity_type' => 'catalog_product',
'input_type' => 'select',
]
);
}
/**
* @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php
* @magentoConfigFixture default/system/full_page_cache/caching_application 2
*
* @return void
*/
public function testCacheInvalidationOnAttributeDelete()
{
$query = $this->getAttributeQuery("dropdown_attribute", "catalog_product");
// check cache missed on first query
$response = $this->assertCacheMissAndReturnResponse($query, []);
$this->assertResponseFields(
$response['body']['customAttributeMetadata']['items'][0],
[
'attribute_code' => 'dropdown_attribute',
'attribute_type' => 'String',
'entity_type' => 'catalog_product',
'input_type' => 'select',
]
);
// assert cache hit on second query
$this->assertCacheHitAndReturnResponse($query, []);
/** @var AttributeRepository $eavAttributeRepo */
$eavAttributeRepo = $this->objectManager->get(AttributeRepository::class);
$attribute = $eavAttributeRepo->get("catalog_product", "dropdown_attribute");
$eavAttributeRepo->delete($attribute);
$this->assertQueryResultIsCacheMissWithError(
$query,
"GraphQL response contains errors: Internal server error"
);
}
/**
* @magentoApiDataFixture Magento/Catalog/_files/dropdown_attribute.php
* @magentoConfigFixture default/system/full_page_cache/caching_application 2
*
* @return void
*/
public function testCacheMissingAttributeParam()
{
$query = $this->getAttributeQueryNoCode("catalog_product");
// check cache missed on each query
$this->assertQueryResultIsCacheMissWithError(
$query,
"Missing attribute_code for the input entity_type: catalog_product."
);
$this->assertQueryResultIsCacheMissWithError(
$query,
"Missing attribute_code for the input entity_type: catalog_product."
);
$query = $this->getAttributeQueryNoEntityType("dropdown_attribute");
// check cache missed on each query
$this->assertQueryResultIsCacheMissWithError(
$query,
"Missing entity_type for the input attribute_code: dropdown_attribute."
);
$this->assertQueryResultIsCacheMissWithError(
$query,
"Missing entity_type for the input attribute_code: dropdown_attribute."
);
}
/**
* Assert that query produces an error and the cache is missed.
*
* @param string $query
* @param string $expectedError
* @return void
* @throws \Exception
*/
private function assertQueryResultIsCacheMissWithError(string $query, string $expectedError)
{
$caughtException = null;
try {
// query for response, expect response to be present in exception
$this->graphQlQueryWithResponseHeaders($query, []);
} catch (ResponseContainsErrorsException $exception) {
$caughtException = $exception;
}
$this->assertInstanceOf(
ResponseContainsErrorsException::class,
$caughtException
);
// cannot use expectException because need to assert the headers
$this->assertStringContainsString(
$expectedError,
$caughtException->getMessage()
);
// assert that it's a miss
$this->assertEquals(
$caughtException->getResponseHeaders()['X-Magento-Cache-Debug'],
'MISS'
);
}
/**
* Test cache invalidation when queried for attribute data of different entity types.
* Required for GraphQL FPC use-case since there is no attribute ID provided in the result.
*
* @magentoConfigFixture default/system/full_page_cache/caching_application 2
*
* @return void
*/
public function testCacheInvalidationMultiEntitySameCode()
{
$queryProduct = $this->getAttributeQuery("name", "catalog_product");
$queryCategory = $this->getAttributeQuery("name", "catalog_category");
// precache both product and category response
$this->assertCacheMissAndReturnResponse($queryProduct, []);
$this->assertCacheMissAndReturnResponse($queryCategory, []);
$eavAttributeRepo = $this->objectManager->get(AttributeRepository::class);
$attribute = $eavAttributeRepo->get("catalog_product", "name");
$eavAttributeRepo->save($attribute);
// assert that product is invalidated for the same code but category is not touched
$this->assertCacheMissAndReturnResponse($queryProduct, []);
$this->assertCacheHitAndReturnResponse($queryCategory, []);
}
/**
* Prepare and return GraphQL query for given entity type and code.
*
* @param string $code
* @param string $entityType
* @return string
*/
private function getAttributeQuery(string $code, string $entityType) : string
{
return <<<QUERY
{
customAttributeMetadata(attributes:
[
{
attribute_code:"{$code}",
entity_type:"{$entityType}"
}
]
)
{
items
{
attribute_code
attribute_type
entity_type
input_type
attribute_options{
label
value
}
}
}
}
QUERY;
}
/**
* Prepare and return GraphQL query for given entity type with no code.
*
* @param string $entityType
*
* @return string
*/
private function getAttributeQueryNoCode(string $entityType) : string
{
return <<<QUERY
{
customAttributeMetadata(attributes:
[
{
entity_type:"{$entityType}"
}
]
)
{
items
{
attribute_code
entity_type
}
}
}
QUERY;
}
/**
* Prepare and return GraphQL query for given code with no entity type.
*
* @param string $code
*
* @return string
*/
private function getAttributeQueryNoEntityType(string $code) : string
{
return <<<QUERY
{
customAttributeMetadata(attributes:
[
{
attribute_code:"{$code}"
}
]
)
{
items
{
attribute_code
entity_type
}
}
}
QUERY;
}
}