Skip to content

Commit

Permalink
Merge pull request #85 from BitBagCommerce/VSF2-54-password-reminder-…
Browse files Browse the repository at this point in the history
…token-fix

Vsf2 54 password reminder token fix
  • Loading branch information
senghe authored Jun 20, 2023
2 parents 807d6e4 + 4375538 commit 11d445e
Show file tree
Hide file tree
Showing 17 changed files with 276 additions and 0 deletions.
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"ext-openssl": "*",
"bitbag/wishlist-plugin": "^3.0",
"gesdinet/jwt-refresh-token-bundle": "^0.12.0",
"php-http/message-factory": "^1.1",
"sylius/sylius": "~1.11.0",
"symfony/mailer": "^6.0",
"webonyx/graphql-php": "^14.9"
},
"require-dev": {
Expand Down
37 changes: 37 additions & 0 deletions features/shop/account/resetting_password.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@resetting_password
Feature: Resetting one customer password
In order to reset my password
As a Customer
I need to be able to reset it by reset query


Background:
Given the store operates on a single channel in "United States"
And the store has locale "en_US"
And there is a customer account "[email protected]"

@graphql
Scenario: Setting token for password reset
Given I prepare password reset request operation for user "[email protected]"
And I send that GraphQL request
Then I should receive a JSON response
And This response should contain pattern '/^\{"data":\{"shop_send_reset_password_emailCustomer":\{"customer":\{"id":"\/api\/v2\/shop\/customers\/(\d+)"\}\}\}\}$/'
And user "[email protected]" should have reset password token set

@graphql
Scenario: Checking for valid reset password token availability
Given I prepare password reset request operation for user "[email protected]"
And I send that GraphQL request
And I prepare check reset password token operation for user's "[email protected]" token
And I send that GraphQL request
Then I should receive a JSON response
And This response should contain pattern '/^\{"data":\{"password_reset_tokenUser":\{"username":"([^"]+)"\}\}\}$/'

@graphql
Scenario: Checking for invalid reset password token availability
Given I prepare password reset request operation for user "[email protected]"
And I send that GraphQL request
And I prepare check reset password token operation for invalid token
And I send that GraphQL request
Then I should receive a JSON response
And This response should contain pattern '/"data":\{"password_reset_tokenUser":null\}/'
1 change: 1 addition & 0 deletions spec/CommandHandler/Account/ResetPasswordHandlerSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public function it_is_invokable(

$user->setPlainPassword($command->newPassword)->shouldBeCalled();
$passwordUpdater->updatePassword($user->getWrappedObject())->shouldBeCalled();
$user->setPasswordResetToken(null)->shouldBeCalled();

$user->getCustomer()->willReturn($customer);

Expand Down
68 changes: 68 additions & 0 deletions spec/Resolver/Query/PasswordResetTokenResolverSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/*
* This file has been created by developers from BitBag.
* Feel free to contact us once you face any issues or want to start
* You can find more information about us on https://bitbag.io and write us
* an email on [email protected].
*/

declare(strict_types=1);


namespace spec\BitBag\SyliusVueStorefront2Plugin\Resolver\Query;

use BitBag\SyliusVueStorefront2Plugin\Resolver\Query\PasswordResetTokenResolver;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;

class PasswordResetTokenResolverSpec extends ObjectBehavior
{
public function let(
UserRepositoryInterface $userRepository
): void {
$this->beConstructedWith(
$userRepository
);
}

public function it_is_initializable(): void
{
$this->shouldHaveType(PasswordResetTokenResolver::class);
}

public function it_returns_user_on_valid_token(
UserRepositoryInterface $userRepository,
ShopUserInterface $user
): void {
$context = [
'args' => [
'passwordResetToken' => "TOKEN",
],
];

$token = $context['args']['passwordResetToken'];

$userRepository->findOneBy(['passwordResetToken' => $token])->willReturn($user);

$this->__invoke(null, $context)->shouldReturn($user);
}

public function it_should_return_null_on_invalid_token(
UserRepositoryInterface $userRepository,
): void {
$context = [
'args' => [
'passwordResetToken' => "TOKEN",
],
];

$token = $context['args']['passwordResetToken'];

$userRepository->findOneBy(['passwordResetToken' => $token])->willReturn(null);

$this->shouldThrow(\RuntimeException::class)
->during('__invoke', [null, $context]);
}
}
1 change: 1 addition & 0 deletions src/CommandHandler/Account/ResetPasswordHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public function __invoke(ResetPassword $command): CustomerInterface
$user->setPlainPassword($command->newPassword);

$this->passwordUpdater->updatePassword($user);
$user->setPasswordResetToken(null);

$customer = $user->getCustomer();
Assert::notNull($customer);
Expand Down
39 changes: 39 additions & 0 deletions src/Resolver/Query/PasswordResetTokenResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file has been created by developers from BitBag.
* Feel free to contact us once you face any issues or want to start
* You can find more information about us on https://bitbag.io and write us
* an email on [email protected].
*/

declare(strict_types=1);

namespace BitBag\SyliusVueStorefront2Plugin\Resolver\Query;

use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;

class PasswordResetTokenResolver implements QueryItemResolverInterface
{
private UserRepositoryInterface $userRepository;

public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}

public function __invoke($item, array $context): ShopUserInterface
{
$token = $context['args']['passwordResetToken'];
/** @var ?ShopUserInterface $user */
$user = $this->userRepository->findOneBy(['passwordResetToken' => $token]);

if (null !== $user) {
return $user;
}

throw new \RuntimeException('Token not found');
}
}
12 changes: 12 additions & 0 deletions src/Resources/api_resources/ShopUser.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ We are hiring developers from all over the world. Join us and start your new, ex
</operation>

<operation name="item_query"/>

<operation name="password_reset_token">
<attribute name="normalization_context">
<attribute name="groups">shop:password_reset_token:read</attribute>
</attribute>
<attribute name="item_query">bitbag.sylius_vue_storefront2plugin.resolver.query.password_reset_token_resolver</attribute>
<attribute name="args">
<attribute name="passwordResetToken">
<attribute name="type">String!</attribute>
</attribute>
</attribute>
</operation>
</graphql>

<property name="id" identifier="true" writable="false" />
Expand Down
3 changes: 3 additions & 0 deletions src/Resources/serialization/ShopUser.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ We are hiring developers from all over the world. Join us and start your new, ex
<group>shop:customer:read</group>
<group>shop:user_login:read</group>
</attribute>
<attribute name="username">
<group>shop:password_reset_token:read</group>
</attribute>
</class>
</serializer>
1 change: 1 addition & 0 deletions src/Resources/services/behat/contexts.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<argument type="service" id="sylius.repository.shop_user" />
<argument type="service" id="bitbag.sylius_vue_storefront2_plugin.factory.shop_user_token_factory" />
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="doctrine.orm.entity_manager" />
</service>

<service id="bitbag.sylius_vue_storefront2_plugin.context.login"
Expand Down
6 changes: 6 additions & 0 deletions src/Resources/services/resolvers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ We are hiring developers from all over the world. Join us and start your new, ex
<tag name="api_platform.graphql.mutation_resolver" />
</service>

<service id="bitbag.sylius_vue_storefront2plugin.resolver.query.password_reset_token_resolver"
class="BitBag\SyliusVueStorefront2Plugin\Resolver\Query\PasswordResetTokenResolver">
<argument type="service" id="sylius.repository.shop_user" />
<tag name="api_platform.graphql.query_resolver" />
</service>

<service id="bitbag.sylius_vue_storefront2_plugin.resolver.order_address_state_resolver"
class="BitBag\SyliusVueStorefront2Plugin\Resolver\OrderAddressStateResolver">
<argument type="service" id="sm.factory" />
Expand Down
7 changes: 7 additions & 0 deletions tests/Application/.env
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ JWT_PASSPHRASE=acme_plugin_development
MAILER_URL=smtp://localhost
###< symfony/swiftmailer-bundle ###

###> symfony/mailer ###
# For Gmail as a transport, use: "gmail://username:password@localhost"
# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode="
# Delivery is disabled by default via "null://null"
MAILER_DSN=null://null
###< symfony/mailer ###

###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
Expand Down
3 changes: 3 additions & 0 deletions tests/Application/config/packages/_sylius.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ sylius_attribute:

sylius_api:
enabled: true

sylius_mailer:
sender_adapter: sylius.email_sender.adapter.symfony_mailer
2 changes: 2 additions & 0 deletions tests/Application/config/packages/framework.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ framework:
mapping:
paths:
- '%kernel.project_dir%/../../src/Resources/serialization'
mailer:
dsn: '%env(MAILER_DSN)%'
9 changes: 9 additions & 0 deletions tests/Behat/Context/GraphqlApiPlatformContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,13 @@ public function iAddFilterToThisOperation(string $filterName, $value, string $ty
$value = $this->castToType($value, $type);
$operation->addFilter($filterName, $value);
}

/**
* @Then This response should contain pattern :pattern
*/
public function responseShouldContainPattern(string $pattern): void{
$response = $this->client->getLastResponse();
$data = stripslashes($response->getContent());
Assert::true((bool)preg_match($pattern, $data));
}
}
73 changes: 73 additions & 0 deletions tests/Behat/Context/Shop/CustomerContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
use ApiPlatform\Core\Api\IriConverterInterface;
use Behat\Behat\Context\Context;
use BitBag\SyliusVueStorefront2Plugin\Factory\ShopUserTokenFactoryInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManagerInterface;
use Sylius\Behat\Service\SharedStorageInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Tests\BitBag\SyliusVueStorefront2Plugin\Behat\Client\GraphqlClient;
use Tests\BitBag\SyliusVueStorefront2Plugin\Behat\Client\GraphqlClientInterface;
Expand All @@ -32,18 +35,33 @@ final class CustomerContext implements Context

private IriConverterInterface $iriConverter;

private EntityManagerInterface $entityManager;

public function __construct(
GraphqlClientInterface $client,
SharedStorageInterface $sharedStorage,
UserRepositoryInterface $userRepository,
ShopUserTokenFactoryInterface $tokenFactory,
IriConverterInterface $iriConverter,
EntityManagerInterface $entityManager,
) {
$this->client = $client;
$this->sharedStorage = $sharedStorage;
$this->userRepository = $userRepository;
$this->tokenFactory = $tokenFactory;
$this->iriConverter = $iriConverter;
$this->entityManager = $entityManager;
}

/**
* @BeforeScenario
*/
public function purgeDatabase()
{
$this->entityManager->getConnection()->getConfiguration()->setSQLLogger(null);
$purger = new ORMPurger($this->entityManager);
$purger->purge();
$this->entityManager->clear();
}

/**
Expand Down Expand Up @@ -158,4 +176,59 @@ public function iPrepareRefreshJwtTokenOperation(): void
$operation->addVariable('refreshToken', $refreshToken);
$this->sharedStorage->set(GraphqlClient::GRAPHQL_OPERATION, $operation);
}

/**
* @Given I prepare password reset request operation for user :email
*/
public function iPreparePasswordResetRequestOperation(string $email): void
{
$mutationBody = '
customer {
id
}';

/** @var LocaleInterface $locale */
$locale = $this->sharedStorage->get('locale');

$operation = $this->client->prepareOperation('shop_send_reset_password_emailCustomer', $mutationBody);
$operation->addVariable('email', $email);
$operation->addVariable('localeCode', $locale->getCode());
$this->sharedStorage->set(GraphqlClient::GRAPHQL_OPERATION, $operation);
}

/**
* @Given /^user "([^"]*)" should have reset password token set$/
*/
public function userShouldHaveResetPasswordTokenSet(string $email)
{
$user = $this->userRepository->findOneByEmail($email);
Assert::notNull($user->getPasswordResetToken());
}

/**
* @Given /^I prepare check reset password token operation for user's "([^"]*)" token$/
*/
public function iPrepareCheckResetPasswordTokenOperationForUserSToken(string $email)
{
$user = $this->userRepository->findOneByEmail($email);
$token = "\"" . $user->getPasswordResetToken() . "\"";
$queryBody = 'username';
$operation = $this->client->prepareQuery('password_reset_tokenUser', $queryBody);

$operation->addFilter('passwordResetToken', $token);
$this->sharedStorage->set(GraphqlClient::GRAPHQL_OPERATION, $operation);
}

/**
* @Given /^I prepare check reset password token operation for invalid token$/
*/
public function iPrepareCheckResetPasswordTokenOperationForUserSInvalidToken()
{
$token = "\"invalid-token\"";
$queryBody = 'username';
$operation = $this->client->prepareQuery('password_reset_tokenUser', $queryBody);

$operation->addFilter('passwordResetToken', $token);
$this->sharedStorage->set(GraphqlClient::GRAPHQL_OPERATION, $operation);
}
}
1 change: 1 addition & 0 deletions tests/Behat/Resources/suites.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ imports:
- suites/account/creating_account.yaml
- suites/account/editing_account.yaml
- suites/account/changing_password.yaml
- suites/account/resetting_password.yaml
- suites/account/managing_addresses.yaml
- suites/wishlist/wishlist_as_guest.yaml
- suites/wishlist/wishlist_as_customer.yaml
11 changes: 11 additions & 0 deletions tests/Behat/Resources/suites/account/resetting_password.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
default:
suites:
graphql_resetting_password:
contexts:
- sylius.behat.context.setup.locale
- sylius.behat.context.setup.customer
- bitbag.sylius_vue_storefront2_plugin.context.graphql
- bitbag.sylius_vue_storefront2_plugin.context.customer
- sylius.behat.context.setup.channel
filters:
tags: "@resetting_password&&@graphql"

0 comments on commit 11d445e

Please sign in to comment.