From 6c4024f264b221e16a620702e1dda20441bef28f Mon Sep 17 00:00:00 2001 From: eliseacornejo Date: Tue, 16 May 2023 15:27:04 +0200 Subject: [PATCH] LYNX-149: Cache identity for attributesMetadata query (#114) * LYNX-149: Cache identity for attributesMetadata query --- .../Model/Output/GetAttributeData.php | 1 + .../CustomAttributeMetadataV2Identity.php | 39 ++ .../Magento/EavGraphQl/etc/schema.graphqls | 2 +- .../CustomAttributesMetadataV2CacheTest.php | 382 ++++++++++++++++++ 4 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Eav/CustomAttributesMetadataV2CacheTest.php diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php index c40e2503cc0dd..8316036ff8b31 100644 --- a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php +++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php @@ -63,6 +63,7 @@ public function execute( int $storeId ): array { return [ + 'id' => $attribute->getAttributeId(), 'uid' => $this->attributeUid->encode($entityType, $attribute->getAttributeCode()), 'code' => $attribute->getAttributeCode(), 'label' => $attribute->getStoreLabel($storeId), diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php new file mode 100644 index 0000000000000..2b67b43d1e9ac --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php @@ -0,0 +1,39 @@ +attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepository::class); + $this->uid = Bootstrap::getObjectManager()->get(Uid::class); + parent::setUp(); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute' + ), + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testCacheHitMiss(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $uidCode = $this->uid->encode('customer', $attribute->getAttributeCode()); + + $query = $this->getAttributeQuery($attribute->getAttributeCode(), "customer"); + $response = $this->assertCacheMissAndReturnResponse($query, []); + $assertionMap = [ + ['response_field' => 'uid', 'expected_value' => $uidCode], + ['response_field' => 'code', 'expected_value' => $attribute->getAttributeCode()], + ['response_field' => 'entity_type', 'expected_value' => 'CUSTOMER'], + ['response_field' => 'frontend_input', 'expected_value' => 'TEXT'] + ]; + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + $response = $this->assertCacheHitAndReturnResponse($query, []); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + } + + #[ + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$'], 'store2'), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute' + ), + ] + public function testCacheMissAndHitDifferentStores(): void + { + /** @var StoreInterface $store2 */ + $store2 = DataFixtureStorageManager::getStorage()->get('store2'); + + /** @var AttributeMetadataInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + $uidCode = $this->uid->encode('customer', $attribute->getAttributeCode()); + + $query = $this->getAttributeQuery($attribute->getAttributeCode(), "customer"); + $response = $this->assertCacheMissAndReturnResponse($query, []); + $assertionMap = [ + ['response_field' => 'uid', 'expected_value' => $uidCode], + ['response_field' => 'code', 'expected_value' => $attribute->getAttributeCode()], + ['response_field' => 'entity_type', 'expected_value' => 'CUSTOMER'], + ['response_field' => 'frontend_input', 'expected_value' => 'TEXT'] + ]; + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + $response = $this->assertCacheHitAndReturnResponse($query, []); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + + // First query execution for a different store should result in a cache miss, while second one should be a hit + $response = $this->assertCacheMissAndReturnResponse($query, ['Store' => $store2->getCode()]); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + $response = $this->assertCacheHitAndReturnResponse($query, ['Store' => $store2->getCode()]); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute_1' + ), + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'boolean' + ], + 'attribute_2' + ), + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testCacheInvalidation(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute_1'); + + /** @var AttributeInterface $attribute2 */ + $attribute2 = DataFixtureStorageManager::getStorage()->get('attribute_2'); + + $query = $this->getAttributeQuery($attribute->getAttributeCode(), "customer"); + // check cache missed on first query + $this->assertCacheMissAndReturnResponse($query, []); + // assert cache hit on second query + $this->assertCacheHitAndReturnResponse($query, []); + + $attribute->setIsRequired(true); + $this->attributeRepository->save($attribute); + // assert cache miss after changes + $this->assertCacheMissAndReturnResponse($query, []); + + $attribute2->setIsRequired(true); + $this->attributeRepository->save($attribute2); + + // assert cache hits on second query after changes, and cache is not invalidated when another entity changed + $this->assertCacheHitAndReturnResponse($query, []); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute' + ), + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testCacheInvalidationOnAttributeDelete() + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + $uidCode = $this->uid->encode('customer', $attribute->getAttributeCode()); + $attributeCode = $attribute->getAttributeCode(); + + $query = $this->getAttributeQuery($attributeCode, "customer"); + + // check cache missed on first query + $response = $this->assertCacheMissAndReturnResponse($query, []); + $assertionMap = [ + ['response_field' => 'uid', 'expected_value' => $uidCode], + ['response_field' => 'code', 'expected_value' => $attributeCode], + ['response_field' => 'entity_type', 'expected_value' => 'CUSTOMER'], + ['response_field' => 'frontend_input', 'expected_value' => 'TEXT'] + ]; + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + + // assert cache hit on second query + $response = $this->assertCacheHitAndReturnResponse($query, []); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['items'][0], $assertionMap); + + $this->attributeRepository->delete($attribute); + $assertionMap = [ + ['response_field' => 'type', 'expected_value' => 'ATTRIBUTE_NOT_FOUND'], + ['response_field' => 'message', 'expected_value' => sprintf( + 'Attribute code "%s" could not be found.', + $attributeCode + )] + ]; + $response = $this->assertCacheMissAndReturnResponse($query, []); + $this->assertResponseFields($response['body']['customAttributeMetadataV2']['errors'][0], $assertionMap); + } + + #[ + DataFixture( + Attribute::class, + [ + 'entity_type_id' => CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + 'frontend_input' => 'text' + ], + 'attribute' + ), + ConfigFixture(Config::XML_PAGECACHE_TYPE, Config::VARNISH) + ] + public function testCacheMissingAttributeParam(): void + { + /** @var AttributeInterface $attribute */ + $attribute = DataFixtureStorageManager::getStorage()->get('attribute'); + + $query = $this->getAttributeQueryNoCode("customer"); + // check cache missed on each query + $this->assertQueryResultIsCacheMissWithError( + $query, + "Missing attribute_code for the input entity_type: customer." + ); + $this->assertQueryResultIsCacheMissWithError( + $query, + "Missing attribute_code for the input entity_type: customer." + ); + + $query = $this->getAttributeQueryNoEntityType($attribute->getAttributeCode()); + // check cache missed on each query + $this->assertQueryResultIsCacheMissWithError( + $query, + sprintf("Missing entity_type for the input attribute_code: %s.", $attribute->getAttributeCode()) + ); + $this->assertQueryResultIsCacheMissWithError( + $query, + sprintf("Missing entity_type for the input attribute_code: %s.", $attribute->getAttributeCode()) + ); + } + + /** + * 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( + 'MISS', + $caughtException->getResponseHeaders()['X-Magento-Cache-Debug'] + ); + } + + /** + * 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 <<