diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductHtmlAttribute.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductHtmlAttribute.php new file mode 100644 index 00000000000..43fb1355c6b --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductHtmlAttribute.php @@ -0,0 +1,56 @@ +outputHelper = $outputHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('"model" value should be specified')); + } + + /* @var $product Product */ + $product = $value['model']; + $fieldName = $field->getName(); + $renderedValue = $this->outputHelper->productAttribute($product, $product->getData($fieldName), $fieldName); + return $renderedValue; + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 9235ec271a3..f63c1a96762 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -248,8 +248,8 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ id: Int @doc(description: "The ID number assigned to the product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") name: String @doc(description: "The product name. Customers use this name to identify the product.") sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer") - description: String @doc(description: "Detailed information about the product. The value can include simple HTML tags.") - short_description: String @doc(description: "A short description of the product. Its use depends on the theme.") + description: String @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductHtmlAttribute") + short_description: String @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductHtmlAttribute") special_price: Float @doc(description: "The discounted price of the product") special_from_date: String @doc(description: "The beginning date that a product has a special price") special_to_date: String @doc(description: "The end date that a product has a special price") @@ -548,6 +548,6 @@ type SortField { } type SortFields @doc(description: "SortFields contains a default value for sort fields and all available sort fields") { - default: String @doc(description: "Default value of sort fields") + default: String @doc(description: "Default value of sort fields") options: [SortField] @doc(description: "Available sort fields") } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer/Account/GenerateCustomerToken.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer/Account/GenerateCustomerToken.php new file mode 100644 index 00000000000..b756a96411a --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer/Account/GenerateCustomerToken.php @@ -0,0 +1,61 @@ +customerTokenService = $customerTokenService; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + try { + if (!isset($args['email'])) { + throw new GraphQlInputException(__('"email" value should be specified')); + } + if (!isset($args['password'])) { + throw new GraphQlInputException(__('"password" value should be specified')); + } + $token = $this->customerTokenService->createCustomerAccessToken($args['email'], $args['password']); + return ['token' => $token]; + } catch (AuthenticationException $e) { + throw new GraphQlAuthorizationException( + __($e->getMessage()) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/composer.json b/app/code/Magento/CustomerGraphQl/composer.json index 290d925215e..c26c83c95be 100644 --- a/app/code/Magento/CustomerGraphQl/composer.json +++ b/app/code/Magento/CustomerGraphQl/composer.json @@ -6,6 +6,7 @@ "php": "~7.1.3||~7.2.0", "magento/module-customer": "*", "magento/module-authorization": "*", + "magento/module-integration": "*", "magento/framework": "*" }, "suggest": { diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index 91b7ef1f9be..2fbf2b12193 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -5,6 +5,14 @@ type Query { customer: Customer @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\Customer") @doc(description: "The customer query returns information about a customer account") } +type Mutation { + generateCustomerToken(email: String!, password: String!): CustomerToken @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\Customer\\Account\\GenerateCustomerToken") @doc(description:"Retrieve Customer token") +} + +type CustomerToken { + token: String @doc(description: "The customer token") +} + type Customer @doc(description: "Customer defines the customer name and address and other details") { created_at: String @doc(description: "Timestamp indicating when the account was created") group_id: Int @doc(description: "The group assigned to the user. Default values are 0 (Not logged in), 1 (General), 2 (Wholesale), and 3 (Retailer)") diff --git a/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteId.php b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteId.php index f30d98342be..37a8fcd494f 100644 --- a/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteId.php +++ b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteId.php @@ -10,6 +10,9 @@ use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResource; +/** + * MaskedQuoteId to QuoteId resolver + */ class MaskedQuoteIdToQuoteId implements MaskedQuoteIdToQuoteIdInterface { /** diff --git a/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteId.php b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteId.php index 5ddadfc22f5..2e802f47cfe 100644 --- a/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteId.php +++ b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteId.php @@ -8,29 +8,40 @@ namespace Magento\Quote\Model; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResource; +/** + * QuoteId to MaskedQuoteId resolver + */ class QuoteIdToMaskedQuoteId implements QuoteIdToMaskedQuoteIdInterface { /** * @var QuoteIdMaskFactory */ private $quoteIdMaskFactory; - /** * @var CartRepositoryInterface */ private $cartRepository; + /** + * @var QuoteIdMaskResource + */ + private $quoteIdMaskResource; + /** * @param QuoteIdMaskFactory $quoteIdMaskFactory * @param CartRepositoryInterface $cartRepository + * @param QuoteIdMaskResource $quoteIdMaskResource */ public function __construct( QuoteIdMaskFactory $quoteIdMaskFactory, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + QuoteIdMaskResource $quoteIdMaskResource ) { $this->quoteIdMaskFactory = $quoteIdMaskFactory; $this->cartRepository = $cartRepository; + $this->quoteIdMaskResource = $quoteIdMaskResource; } /** @@ -42,8 +53,9 @@ public function execute(int $quoteId): string $this->cartRepository->get($quoteId); $quoteIdMask = $this->quoteIdMaskFactory->create(); - $quoteIdMask->setQuoteId($quoteId)->save(); + $this->quoteIdMaskResource->load($quoteIdMask, $quoteId, 'quote_id'); + $maskedId = $quoteIdMask->getMaskedId() ?? ''; - return $quoteIdMask->getMaskedId(); + return $maskedId; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart/CreateEmptyCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart/CreateEmptyCart.php index fcf23dd9f68..78f96383132 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart/CreateEmptyCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart/CreateEmptyCart.php @@ -14,6 +14,7 @@ use Magento\Quote\Api\CartManagementInterface; use Magento\Quote\Api\GuestCartManagementInterface; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; /** * @inheritdoc @@ -24,7 +25,6 @@ class CreateEmptyCart implements ResolverInterface * @var CartManagementInterface */ private $cartManagement; - /** * @var GuestCartManagementInterface */ @@ -40,22 +40,30 @@ class CreateEmptyCart implements ResolverInterface */ private $userContext; + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + /** * @param CartManagementInterface $cartManagement * @param GuestCartManagementInterface $guestCartManagement * @param UserContextInterface $userContext * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId + * @param QuoteIdMaskFactory $quoteIdMaskFactory */ public function __construct( CartManagementInterface $cartManagement, GuestCartManagementInterface $guestCartManagement, UserContextInterface $userContext, - QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId, + QuoteIdMaskFactory $quoteIdMaskFactory ) { $this->cartManagement = $cartManagement; $this->guestCartManagement = $guestCartManagement; $this->userContext = $userContext; $this->quoteIdToMaskedId = $quoteIdToMaskedId; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; } /** @@ -67,7 +75,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (0 !== $customerId && null !== $customerId) { $quoteId = $this->cartManagement->createEmptyCartForCustomer($customerId); - $maskedQuoteId = $this->quoteIdToMaskedId->execute($quoteId); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quoteId); + + if (empty($maskedQuoteId)) { + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quoteId)->save(); + $maskedQuoteId = $quoteIdMask->getMaskedId(); + } } else { $maskedQuoteId = $this->guestCartManagement->createEmptyCart(); } diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php index 488f1281ce3..5bdaf362725 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php @@ -75,7 +75,7 @@ public function resolve( if ($urlRewrite) { $result = [ 'id' => $urlRewrite->getEntityId(), - 'canonical_url' => $urlRewrite->getTargetPath(), + 'relative_url' => $urlRewrite->getTargetPath(), 'type' => $this->sanitizeType($urlRewrite->getEntityType()) ]; } diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index 38f1d9c6563..e9a39617774 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -1,14 +1,14 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. -type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `canonical_url`, and `type` attributes") { +type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `relative_url`, and `type` attributes") { id: Int @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") - canonical_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") + relative_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") type: UrlRewriteEntityTypeEnum @doc(description: "One of PRODUCT, CATEGORY, or CMS_PAGE.") } type Query { - urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") @doc(description: "The urlResolver query returns the canonical URL for a specified product, category or CMS page") + urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page") } enum UrlRewriteEntityTypeEnum { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductWithDescriptionDirectivesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductWithDescriptionDirectivesTest.php new file mode 100644 index 00000000000..8e9bc4dfa28 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductWithDescriptionDirectivesTest.php @@ -0,0 +1,65 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Cms/_files/block.php + */ + public function testHtmlDirectivesRendered() + { + $productSku = 'simple'; + $cmsBlockId = 'fixture_block'; + $assertionCmsBlockText = 'Fixture Block Title'; + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var ProductInterface $product */ + $product = $productRepository->get($productSku, false, null, true); + $product->setDescription('Test: {{block id="' . $cmsBlockId . '"}}'); + $product->setShortDescription('Test: {{block id="' . $cmsBlockId . '"}}'); + $productRepository->save($product); + + $query = <<graphQlQuery($query); + + self::assertContains($assertionCmsBlockText, $response['products']['items'][0]['description']); + self::assertNotContains('{{block id', $response['products']['items'][0]['description']); + self::assertContains($assertionCmsBlockText, $response['products']['items'][0]['short_description']); + self::assertNotContains('{{block id', $response['products']['items'][0]['short_description']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php new file mode 100644 index 00000000000..ae28e23a28b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php @@ -0,0 +1,71 @@ +graphQlQuery($mutation); + $this->assertArrayHasKey('generateCustomerToken', $response); + $this->assertInternalType('array', $response['generateCustomerToken']); + } + + /** + * Verify customer with invalid credentials + */ + public function testGenerateCustomerTokenWithInvalidCredentials() + { + $userName = 'customer@example.com'; + $password = 'bad-password'; + + $mutation + = <<expectException(\Exception::class); + $this->expectExceptionMessage('GraphQL response contains errors: The account sign-in' . ' ' . + 'was incorrect or your account is disabled temporarily. Please wait and try again later.'); + $this->graphQlQuery($mutation); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php index 0bd24ee7bc8..370121a1dad 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php @@ -12,6 +12,9 @@ use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\Cms\Helper\Page as PageHelper; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Test the GraphQL endpoint's URLResolver query to verify canonical URL's are correctly returned. @@ -28,7 +31,7 @@ protected function setUp() } /** - * Tests if target_path(canonical_url) is resolved for Product entity + * Tests if target_path(relative_url) is resolved for Product entity * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ @@ -57,7 +60,7 @@ public function testProductUrlResolver() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -65,12 +68,12 @@ public function testProductUrlResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } /** - * Tests the use case where canonical_url is provided as resolver input in the Query + * Tests the use case where relative_url is provided as resolver input in the Query * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ @@ -101,7 +104,7 @@ public function testProductUrlWithCanonicalUrlInput() urlResolver(url:"{$canonicalPath}") { id - canonical_url + relative_url type } } @@ -109,7 +112,7 @@ public function testProductUrlWithCanonicalUrlInput() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -144,7 +147,7 @@ public function testCategoryUrlResolver() urlResolver(url:"{$urlPath2}") { id - canonical_url + relative_url type } } @@ -152,7 +155,7 @@ public function testCategoryUrlResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -180,14 +183,14 @@ public function testCMSPageUrlResolver() urlResolver(url:"{$requestPath}") { id - canonical_url + relative_url type } } QUERY; $response = $this->graphQlQuery($query); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); } @@ -223,7 +226,7 @@ public function testProductUrlRewriteResolver() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -231,7 +234,7 @@ public function testProductUrlRewriteResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -263,7 +266,7 @@ public function testInvalidUrlResolverInput() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -304,7 +307,7 @@ public function testCategoryUrlWithLeadingSlash() urlResolver(url:"/{$urlPath}") { id - canonical_url + relative_url type } } @@ -312,7 +315,7 @@ public function testCategoryUrlWithLeadingSlash() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -321,22 +324,35 @@ public function testCategoryUrlWithLeadingSlash() */ public function testResolveSlash() { + /** @var \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfigInterface */ + $scopeConfigInterface = $this->objectManager->get(ScopeConfigInterface::class); + $homePageIdentifier = $scopeConfigInterface->getValue( + PageHelper::XML_PATH_HOME_PAGE, + ScopeInterface::SCOPE_STORE + ); + /** @var \Magento\Cms\Model\Page $page */ + $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); + $page->load($homePageIdentifier); + $homePageId = $page->getId(); + /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ + $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); + /** @param \Magento\Cms\Api\Data\PageInterface $page */ + $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); $query = <<graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals(2, $response['urlResolver']['id']); - $this->assertEquals('cms/page/view/page_id/2', $response['urlResolver']['canonical_url']); + $this->assertEquals($homePageId, $response['urlResolver']['id']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); } }