add validation, fix register customer route, add missing mail send

This commit is contained in:
arti0090 2021-03-16 16:39:12 +01:00
parent 5ba7a4e2a0
commit 3272f454de
23 changed files with 378 additions and 64 deletions

View file

@ -17,7 +17,7 @@ Feature: Verifying account's email address
And I should be able to log in as "valkyrie@cain.com" with "sylius" password And I should be able to log in as "valkyrie@cain.com" with "sylius" password
And my account should be verified And my account should be verified
@ui @ui @api
Scenario: Being unable to verify with invalid token Scenario: Being unable to verify with invalid token
When I try to verify using "twinklelittlestar" token When I try to verify using "twinklelittlestar" token
Then I should be notified that the verification token is invalid Then I should be notified that the verification token is invalid
@ -45,14 +45,14 @@ Feature: Verifying account's email address
And I have already verified my account And I have already verified my account
Then I should not be able to resend the verification email Then I should not be able to resend the verification email
@ui @email @ui @email @api
Scenario: Receiving account verification email after registration Scenario: Receiving account verification email after registration
When I register with email "ghastly@bespoke.com" and password "suitsarelife" When I register with email "ghastly@bespoke.com" and password "suitsarelife"
Then I should be notified that my account has been created and the verification email has been sent Then I should be notified that my account has been created and the verification email has been sent
And 2 emails should be sent to "ghastly@bespoke.com" And 2 emails should be sent to "ghastly@bespoke.com"
But I should not be able to log in as "ghastly@bespoke.com" with "suitsarelife" password But I should not be able to log in as "ghastly@bespoke.com" with "suitsarelife" password
@ui @email @ui @email @api
Scenario: Do not send verification email when account verification on the channel is not required Scenario: Do not send verification email when account verification on the channel is not required
Given "United States" channel has account verification disabled Given "United States" channel has account verification disabled
When I register with email "ghastly@bespoke.com" and password "suitsarelife" When I register with email "ghastly@bespoke.com" and password "suitsarelife"

View file

@ -44,6 +44,14 @@ final class EmailContext implements Context
); );
} }
/**
* @Then :count email(s) should be sent to :recipient
*/
public function numberOfEmailsShouldBeSentTo(int $count, string $recipient): void
{
Assert::same($this->emailChecker->countMessagesTo($recipient), $count);
}
private function assertEmailContainsMessageTo(string $message, string $recipient): void private function assertEmailContainsMessageTo(string $message, string $recipient): void
{ {
Assert::true($this->emailChecker->hasMessageTo($message, $recipient)); Assert::true($this->emailChecker->hasMessageTo($message, $recipient));

View file

@ -48,6 +48,9 @@ final class CustomerContext implements Context
/** @var ShopSecurityContext */ /** @var ShopSecurityContext */
private $shopApiSecurityContext; private $shopApiSecurityContext;
/** @var string */
private $verificationToken = '';
public function __construct( public function __construct(
ApiClientInterface $customerClient, ApiClientInterface $customerClient,
ApiClientInterface $orderShopClient, ApiClientInterface $orderShopClient,
@ -186,12 +189,25 @@ final class CustomerContext implements Context
*/ */
public function iTryToVerifyMyAccountUsingTheLinkFromEmail(ShopUserInterface $user): void public function iTryToVerifyMyAccountUsingTheLinkFromEmail(ShopUserInterface $user): void
{ {
$request = Request::custom( $this->verificationToken = $user->getEmailVerificationToken();
\sprintf('/api/v2/shop/account-verification-requests/%s', (string) $user->getEmailVerificationToken()), $this->verifyAccount($this->verificationToken);
HttpRequest::METHOD_PATCH, }
);
$this->customerClient->executeCustomRequest($request); /**
* @When I (try to )verify using :token token
*/
public function iTryToVerifyUsing(string $token): void
{
$this->verifyAccount($token);
}
/**
* @When I register with email :email and password :password
*/
public function iRegisterWithEmailAndPassword(string $email, string $password): void
{
$this->iRegisterThisAccount($email, $password);
$this->loginContext->iLogInAsWithPassword($email, $password);
} }
/** /**
@ -287,6 +303,17 @@ final class CustomerContext implements Context
); );
} }
/**
* @Then I should be notified that the verification token is invalid
*/
public function iShouldBeNotifiedThatTheVerificationTokenIsInvalid(): void
{
$this->isViolationWithMessageInResponse(
$this->customerClient->getLastResponse(),
sprintf('There is no shop user with %s email verification token.', $this->verificationToken)
);
}
/** /**
* @When I browse my orders * @When I browse my orders
*/ */
@ -337,20 +364,10 @@ final class CustomerContext implements Context
$this->responseChecker->isCreationSuccessful($this->customerClient->getLastResponse()); $this->responseChecker->isCreationSuccessful($this->customerClient->getLastResponse());
} }
private function isViolationWithMessageInResponse(Response $response, string $message): bool
{
$violations = $this->responseChecker->getResponseContent($response)['violations'];
foreach ($violations as $violation) {
if ($violation['message'] === $message) {
return true;
}
}
return false;
}
/** /**
* @Then I should be notified that my password has been successfully changed * @Then I should be notified that my password has been successfully changed
* @Then I should be notified that new account has been successfully created
* @Then I should be notified that my account has been created and the verification email has been sent
*/ */
public function iShouldBeNotifiedThatItHasBeenSuccessfullyChanged(): void public function iShouldBeNotifiedThatItHasBeenSuccessfullyChanged(): void
{ {
@ -408,8 +425,48 @@ final class CustomerContext implements Context
$user = $this->sharedStorage->get('user'); $user = $this->sharedStorage->get('user');
$this->loginContext->iLogInAsWithPassword($user->getEmail(), 'sylius'); $this->loginContext->iLogInAsWithPassword($user->getEmail(), 'sylius');
$response = $this->customerClient->show((string) $user->getId()); $response = $this->customerClient->show((string) $user->getCustomer()->getId());
Assert::true($this->responseChecker->getResponseContent($response)['user']['verified']); Assert::true($this->responseChecker->getResponseContent($response)['user']['verified']);
} }
private function isViolationWithMessageInResponse(Response $response, string $message): bool
{
$violations = $this->responseChecker->getResponseContent($response)['violations'];
foreach ($violations as $violation) {
if ($violation['message'] === $message) {
return true;
}
}
return false;
}
private function verifyAccount(string $token): void
{
$request = Request::custom(
\sprintf('/api/v2/shop/account-verification-requests/%s', $token),
HttpRequest::METHOD_PATCH,
);
$this->customerClient->executeCustomRequest($request);
}
private function iRegisterThisAccount(?string $email = 'example@example.com', ?string $password = 'example'): void
{
$request = Request::create(
'shop',
'customers',
'',
);
$request->setContent([
'firstName' => 'First',
'lastName' => 'Last',
'email' => $email,
'password' => $password,
]);
$this->customerClient->executeCustomRequest($request);
}
} }

View file

@ -211,6 +211,16 @@ final class LoginContext implements Context
$this->iShouldBeLoggedIn(); $this->iShouldBeLoggedIn();
} }
/**
* @Then I should not be able to log in as :email with :password password
*/
public function iShouldNotBeAbleToLogInAsWithPassword(string $email, string $password): void
{
$this->iLogInAsWithPassword($email, $password);
$this->iShouldNotBeLoggedIn();
}
private function addLocaleCode(string $localeCode): void private function addLocaleCode(string $localeCode): void
{ {
$this->request->updateContent(['localeCode' => $localeCode]); $this->request->updateContent(['localeCode' => $localeCode]);

View file

@ -112,7 +112,7 @@ final class RegistrationContext implements Context
{ {
$this->client->request( $this->client->request(
'POST', 'POST',
'/api/v2/shop/customers/', '/api/v2/shop/customers',
[], [],
[], [],
['HTTP_ACCEPT' => 'application/ld+json', 'CONTENT_TYPE' => 'application/ld+json'], ['HTTP_ACCEPT' => 'application/ld+json', 'CONTENT_TYPE' => 'application/ld+json'],

View file

@ -16,6 +16,7 @@ default:
- sylius.behat.context.setup.shop_api_security - sylius.behat.context.setup.shop_api_security
- sylius.behat.context.setup.user - sylius.behat.context.setup.user
- sylius.behat.context.api.email
- sylius.behat.context.api.shop.customer - sylius.behat.context.api.shop.customer
- sylius.behat.context.api.shop.login - sylius.behat.context.api.shop.login

View file

@ -22,6 +22,8 @@ use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\Resource\Factory\FactoryInterface; use Sylius\Component\Resource\Factory\FactoryInterface;
use Sylius\Component\User\Security\Generator\GeneratorInterface; use Sylius\Component\User\Security\Generator\GeneratorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
@ -46,13 +48,17 @@ final class RegisterShopUserHandler implements MessageHandlerInterface
/** @var MessageBusInterface */ /** @var MessageBusInterface */
private $commandBus; private $commandBus;
/** @var EventDispatcherInterface */
private $eventDispatcher;
public function __construct( public function __construct(
FactoryInterface $shopUserFactory, FactoryInterface $shopUserFactory,
ObjectManager $shopUserManager, ObjectManager $shopUserManager,
CustomerProviderInterface $customerProvider, CustomerProviderInterface $customerProvider,
ChannelRepositoryInterface $channelRepository, ChannelRepositoryInterface $channelRepository,
GeneratorInterface $tokenGenerator, GeneratorInterface $tokenGenerator,
MessageBusInterface $commandBus MessageBusInterface $commandBus,
EventDispatcherInterface $eventDispatcher
) { ) {
$this->shopUserFactory = $shopUserFactory; $this->shopUserFactory = $shopUserFactory;
$this->shopUserManager = $shopUserManager; $this->shopUserManager = $shopUserManager;
@ -60,6 +66,7 @@ final class RegisterShopUserHandler implements MessageHandlerInterface
$this->channelRepository = $channelRepository; $this->channelRepository = $channelRepository;
$this->tokenGenerator = $tokenGenerator; $this->tokenGenerator = $tokenGenerator;
$this->commandBus = $commandBus; $this->commandBus = $commandBus;
$this->eventDispatcher = $eventDispatcher;
} }
public function __invoke(RegisterShopUser $command): void public function __invoke(RegisterShopUser $command): void
@ -82,6 +89,11 @@ final class RegisterShopUserHandler implements MessageHandlerInterface
/** @var ChannelInterface $channel */ /** @var ChannelInterface $channel */
$channel = $this->channelRepository->findOneByCode($command->channelCode); $channel = $this->channelRepository->findOneByCode($command->channelCode);
$this->shopUserManager->persist($user);
$this->shopUserManager->flush();
$this->eventDispatcher->dispatch(new GenericEvent($customer), 'sylius.customer.post_register');
if ($channel->isAccountVerificationRequired()) { if ($channel->isAccountVerificationRequired()) {
$token = $this->tokenGenerator->generate(); $token = $this->tokenGenerator->generate();
$user->setEmailVerificationToken($token); $user->setEmailVerificationToken($token);
@ -95,6 +107,5 @@ final class RegisterShopUserHandler implements MessageHandlerInterface
$user->setEnabled(true); $user->setEnabled(true);
} }
$this->shopUserManager->persist($user);
} }
} }

View file

@ -22,7 +22,7 @@
<collectionOperations> <collectionOperations>
<collectionOperation name="shop_post"> <collectionOperation name="shop_post">
<attribute name="method">POST</attribute> <attribute name="method">POST</attribute>
<attribute name="path">/shop/customers/</attribute> <attribute name="path">/shop/customers</attribute>
<attribute name="openapi_context"> <attribute name="openapi_context">
<attribute name="summary">Registers a new customer</attribute> <attribute name="summary">Registers a new customer</attribute>
</attribute> </attribute>

View file

@ -18,6 +18,7 @@
<resource class="Sylius\Bundle\ApiBundle\Command\VerifyCustomerAccount" shortName="VerifyCustomerAccount"> <resource class="Sylius\Bundle\ApiBundle\Command\VerifyCustomerAccount" shortName="VerifyCustomerAccount">
<attribute name="route_prefix">shop</attribute> <attribute name="route_prefix">shop</attribute>
<attribute name="messenger">input</attribute> <attribute name="messenger">input</attribute>
<attribute name="validation_groups">sylius</attribute>
<collectionOperations /> <collectionOperations />

View file

@ -23,6 +23,7 @@
<argument type="service" id="sylius.repository.channel" /> <argument type="service" id="sylius.repository.channel" />
<argument type="service" id="sylius.shop_user.token_generator.email_verification" /> <argument type="service" id="sylius.shop_user.token_generator.email_verification" />
<argument type="service" id="sylius_default.bus" /> <argument type="service" id="sylius_default.bus" />
<argument type="service" id="event_dispatcher" />
<tag name="messenger.message_handler" bus="sylius_default.bus"/> <tag name="messenger.message_handler" bus="sylius_default.bus"/>
</service> </service>
@ -154,5 +155,12 @@
<argument type="service" id="sylius.email_sender" /> <argument type="service" id="sylius.email_sender" />
<tag name="messenger.message_handler" bus="sylius_default.bus" /> <tag name="messenger.message_handler" bus="sylius_default.bus" />
</service> </service>
<service id="Sylius\Bundle\ApiBundle\CommandHandler\SendUserRegistrationEmailHandler">
<argument type="service" id="sylius.repository.shop_user" />
<argument type="service" id="sylius.repository.channel" />
<argument type="service" id="sylius.email_sender" />
<tag name="messenger.message_handler" bus="sylius_default.bus" />
</service>
</services> </services>
</container> </container>

View file

@ -58,5 +58,10 @@
<argument type="service" id="sylius.promotion_coupon_eligibility_checker" /> <argument type="service" id="sylius.promotion_coupon_eligibility_checker" />
<tag name="validator.constraint_validator" alias="sylius_api_promotion_coupon_eligibility" /> <tag name="validator.constraint_validator" alias="sylius_api_promotion_coupon_eligibility" />
</service> </service>
<service id="sylius.api.validator.account_verification_token_eligibility" class="Sylius\Bundle\ApiBundle\Validator\Constraints\AccountVerificationTokenEligibilityValidator">
<argument type="service" id="sylius.repository.shop_user" />
<tag name="validator.constraint_validator" alias="sylius_api_validator_account_verification_token_eligibility" />
</service>
</services> </services>
</container> </container>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of the Sylius package.
(c) Paweł Jędrzejewski
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">
<class name="Sylius\Bundle\ApiBundle\Command\VerifyCustomerAccount">
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\AccountVerificationTokenEligibility">
<option name="groups">
<value>sylius</value>
</option>
</constraint>
</class>
</constraint-mapping>

View file

@ -1,3 +1,6 @@
sylius: sylius:
account:
invalid_verification_token: 'There is no shop user with %verificationToken% email verification token.'
order: order:
not_empty: 'An empty order cannot be completed.' not_empty: 'An empty order cannot be completed.'

View file

@ -27,6 +27,9 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final class SendAccountVerificationEmailHandlerTest extends KernelTestCase final class SendAccountVerificationEmailHandlerTest extends KernelTestCase
{ {
/**
* @test
*/
public function it_sends_account_verification_token_email_without_hostname(): void public function it_sends_account_verification_token_email_without_hostname(): void
{ {
$container = self::bootKernel()->getContainer(); $container = self::bootKernel()->getContainer();
@ -78,6 +81,9 @@ final class SendAccountVerificationEmailHandlerTest extends KernelTestCase
)); ));
} }
/**
* @test
*/
public function it_sends_account_verification_token_email_with_hostname(): void public function it_sends_account_verification_token_email_with_hostname(): void
{ {
$container = self::bootKernel()->getContainer(); $container = self::bootKernel()->getContainer();

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/** @experimental */
final class AccountVerificationTokenEligibility extends Constraint
{
/** @var string */
public $message = 'sylius.account.invalid_verification_token';
public function validatedBy(): string
{
return 'sylius_api_validator_account_verification_token_eligibility';
}
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View file

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
use Sylius\Bundle\ApiBundle\Command\VerifyCustomerAccount;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Sylius\Component\User\Model\UserInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/** @experimental */
final class AccountVerificationTokenEligibilityValidator extends ConstraintValidator
{
/** @var RepositoryInterface */
private $shopUserRepository;
public function __construct(RepositoryInterface $shopUserRepository)
{
$this->shopUserRepository = $shopUserRepository;
}
/** @param VerifyCustomerAccount|mixed $value */
public function validate($value, Constraint $constraint)
{
/** @var UserInterface|null $user */
$user = $this->shopUserRepository->findOneBy(['emailVerificationToken' => $value->token]);
if (null === $user) {
$this->context->addViolation(
$constraint->message,
['%verificationToken%' => $value->token]
);
}
}
}

View file

@ -24,6 +24,8 @@ use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\Resource\Factory\FactoryInterface; use Sylius\Component\Resource\Factory\FactoryInterface;
use Sylius\Component\User\Security\Generator\GeneratorInterface; use Sylius\Component\User\Security\Generator\GeneratorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
@ -36,7 +38,8 @@ final class RegisterShopUserHandlerSpec extends ObjectBehavior
CustomerProviderInterface $customerProvider, CustomerProviderInterface $customerProvider,
ChannelRepositoryInterface $channelRepository, ChannelRepositoryInterface $channelRepository,
GeneratorInterface $generator, GeneratorInterface $generator,
MessageBusInterface $commandBus MessageBusInterface $commandBus,
EventDispatcherInterface $eventDispatcher
): void { ): void {
$this->beConstructedWith( $this->beConstructedWith(
$shopUserFactory, $shopUserFactory,
@ -44,7 +47,8 @@ final class RegisterShopUserHandlerSpec extends ObjectBehavior
$customerProvider, $customerProvider,
$channelRepository, $channelRepository,
$generator, $generator,
$commandBus $commandBus,
$eventDispatcher
); );
} }
@ -62,7 +66,8 @@ final class RegisterShopUserHandlerSpec extends ObjectBehavior
ChannelRepositoryInterface $channelRepository, ChannelRepositoryInterface $channelRepository,
ChannelInterface $channel, ChannelInterface $channel,
GeneratorInterface $generator, GeneratorInterface $generator,
MessageBusInterface $commandBus MessageBusInterface $commandBus,
EventDispatcherInterface $eventDispatcher
): void { ): void {
$command = new RegisterShopUser('Will', 'Smith', 'WILL.SMITH@example.com', 'iamrobot', '+13104322400'); $command = new RegisterShopUser('Will', 'Smith', 'WILL.SMITH@example.com', 'iamrobot', '+13104322400');
$command->setChannelCode('CHANNEL_CODE'); $command->setChannelCode('CHANNEL_CODE');
@ -85,10 +90,12 @@ final class RegisterShopUserHandlerSpec extends ObjectBehavior
$generator->generate()->willReturn('TOKEN'); $generator->generate()->willReturn('TOKEN');
$shopUser->setEmailVerificationToken('TOKEN')->shouldBeCalled(); $shopUser->setEmailVerificationToken('TOKEN')->shouldBeCalled();
$sendEmailCommand = new SendAccountVerificationEmail('WILL.SMITH@example.com', 'en_US', 'CHANNEL_CODE');
$commandBus->dispatch($sendEmailCommand)->shouldBeCalled()->willReturn(new Envelope($sendEmailCommand));
$shopUserManager->persist($shopUser)->shouldBeCalled(); $shopUserManager->persist($shopUser)->shouldBeCalled();
$shopUserManager->flush()->shouldBeCalled();
$eventDispatcher->dispatch(new GenericEvent($customer->getWrappedObject()), 'sylius.customer.post_register')->shouldBeCalled();
$sendVerificationEmailCommand = new SendAccountVerificationEmail('WILL.SMITH@example.com', 'en_US', 'CHANNEL_CODE');
$commandBus->dispatch($sendVerificationEmailCommand)->shouldBeCalled()->willReturn(new Envelope($sendVerificationEmailCommand));
$this($command); $this($command);
} }
@ -100,7 +107,8 @@ final class RegisterShopUserHandlerSpec extends ObjectBehavior
ShopUserInterface $shopUser, ShopUserInterface $shopUser,
CustomerInterface $customer, CustomerInterface $customer,
ChannelRepositoryInterface $channelRepository, ChannelRepositoryInterface $channelRepository,
ChannelInterface $channel ChannelInterface $channel,
EventDispatcherInterface $eventDispatcher
): void { ): void {
$command = new RegisterShopUser('Will', 'Smith', 'WILL.SMITH@example.com', 'iamrobot', '+13104322400'); $command = new RegisterShopUser('Will', 'Smith', 'WILL.SMITH@example.com', 'iamrobot', '+13104322400');
$command->setChannelCode('CHANNEL_CODE'); $command->setChannelCode('CHANNEL_CODE');
@ -118,11 +126,14 @@ final class RegisterShopUserHandlerSpec extends ObjectBehavior
$customer->setPhoneNumber('+13104322400')->shouldBeCalled(); $customer->setPhoneNumber('+13104322400')->shouldBeCalled();
$customer->setUser($shopUser)->shouldBeCalled(); $customer->setUser($shopUser)->shouldBeCalled();
$shopUserManager->persist($shopUser)->shouldBeCalled();
$shopUserManager->flush()->shouldBeCalled();
$eventDispatcher->dispatch(new GenericEvent($customer->getWrappedObject()), 'sylius.customer.post_register')->shouldBeCalled();
$channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel); $channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel);
$channel->isAccountVerificationRequired()->willReturn(false); $channel->isAccountVerificationRequired()->willReturn(false);
$shopUser->setEnabled(true); $shopUser->setEnabled(true);
$shopUserManager->persist($shopUser)->shouldBeCalled();
$this($command); $this($command);
} }
@ -133,7 +144,8 @@ final class RegisterShopUserHandlerSpec extends ObjectBehavior
CustomerProviderInterface $customerProvider, CustomerProviderInterface $customerProvider,
ShopUserInterface $shopUser, ShopUserInterface $shopUser,
CustomerInterface $customer, CustomerInterface $customer,
ShopUserInterface $existingShopUser ShopUserInterface $existingShopUser,
EventDispatcherInterface $eventDispatcher
): void { ): void {
$shopUserFactory->createNew()->willReturn($shopUser); $shopUserFactory->createNew()->willReturn($shopUser);
$customerProvider->provide('WILL.SMITH@example.com')->willReturn($customer); $customerProvider->provide('WILL.SMITH@example.com')->willReturn($customer);
@ -141,6 +153,8 @@ final class RegisterShopUserHandlerSpec extends ObjectBehavior
$customer->getUser()->willReturn($existingShopUser); $customer->getUser()->willReturn($existingShopUser);
$shopUserManager->persist($shopUser)->shouldNotBeCalled(); $shopUserManager->persist($shopUser)->shouldNotBeCalled();
$shopUserManager->flush()->shouldNotBeCalled();
$eventDispatcher->dispatch(new GenericEvent($customer->getWrappedObject()), 'sylius.customer.post_register')->shouldNotBeCalled();
$this $this
->shouldThrow(\DomainException::class) ->shouldThrow(\DomainException::class)

View file

@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\VerifyCustomerAccount;
use Sylius\Bundle\ApiBundle\Validator\Constraints\AccountVerificationTokenEligibility;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class AccountVerificationTokenEligibilityValidatorSpec extends ObjectBehavior
{
function let(RepositoryInterface $shopUserRepository): void
{
$this->beConstructedWith($shopUserRepository);
}
function it_is_a_constraint_validator(): void
{
$this->shouldImplement(ConstraintValidatorInterface::class);
}
function it_adds_violation_if_account_is_null(
RepositoryInterface $shopUserRepository,
ExecutionContextInterface $executionContext
): void {
$constraint = new AccountVerificationTokenEligibility();
$value = new VerifyCustomerAccount('TOKEN');
$this->initialize($executionContext);
$shopUserRepository->findOneBy(['emailVerificationToken' => 'TOKEN'])->willReturn(null);
$executionContext
->addViolation(
'sylius.account.invalid_verification_token',
['%verificationToken%' => 'TOKEN']
)
->shouldBeCalled()
;
$this->validate($value, $constraint);
}
function it_does_nothing_if_account_has_been_found(
RepositoryInterface $shopUserRepository,
ExecutionContextInterface $executionContext,
ShopUserInterface $user
): void {
$constraint = new AccountVerificationTokenEligibility();
$value = new VerifyCustomerAccount('TOKEN');
$this->initialize($executionContext);
$shopUserRepository->findOneBy(['emailVerificationToken' => 'TOKEN'])->willReturn($user);
$executionContext
->addViolation(
'sylius.account.invalid_verification_token',
['%verificationToken%' => 'TOKEN']
)
->shouldNotBeCalled()
;
$this->validate($value, $constraint);
}
}

View file

@ -9,9 +9,12 @@ sylius_mailer:
order_confirmation: order_confirmation:
subject: sylius.emails.order_confirmation.subject subject: sylius.emails.order_confirmation.subject
template: "@SyliusCore/Email/orderConfirmation.html.twig" template: "@SyliusCore/Email/orderConfirmation.html.twig"
user_registration:
subject: sylius.emails.user_registration.subject
template: "@SyliusCore/Email/userRegistration.html.twig"
password_reset: password_reset:
subject: sylius.emails.user.password_reset.subject subject: sylius.emails.user.password_reset.subject
template: "@SyliusCore/Email/passwordReset.html.twig" template: "@SyliusCore/Email/passwordReset.html.twig"
account_verification_token: account_verification_token:
subject: sylius.emails.user.verification_token.subject subject: sylius.emails.user.verification_token.subject
template: "@SyliusShop/Email/verification.html.twig" template: "@SyliusCore/Email/accountVerification.html.twig"

View file

@ -244,6 +244,14 @@ sylius:
by_zones_and_channel: By zones and channel by_zones_and_channel: By zones and channel
email: email:
account_verification_token: user_registration:
subject: Email address verification start_shopping: 'Start shopping'
subject: 'User registration'
welcome_to_our_store: 'Welcome to our store!'
you_have_just_been_registered: 'You have just been registered. Thank you'
verification_token:
hello: 'Hello'
message: "Verify your account with token: " message: "Verify your account with token: "
subject: 'Email address verification'
to_verify_your_email_address: 'To verify your email address - click the link below'
verify_your_email_address: 'Verify your email address'

View file

@ -1,11 +1,7 @@
{% extends '@SyliusCore/Email/layout.html.twig' %} {% extends '@SyliusCore/Email/layout.html.twig' %}
{% block subject %} {% block subject %}
{% if sylius_bundle_loaded_checker('SyliusShopBundle') %}
{{ 'sylius.email.verification_token.subject'|trans({}, null, localeCode) }} {{ 'sylius.email.verification_token.subject'|trans({}, null, localeCode) }}
{% else %}
{{ 'sylius.email.account_verification_token.subject'|trans({}, null, localeCode) }}
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -25,7 +21,7 @@
</div> </div>
{% else %} {% else %}
<div style="text-align: center;"> <div style="text-align: center;">
<span>{{ 'sylius.email.account_verification_token.message'|trans({}, null, localeCode) }}{{ user.emailVerificationToken }}</span> <span>{{ 'sylius.email.verification_token.message'|trans({}, null, localeCode) }}{{ user.emailVerificationToken }}</span>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,23 @@
{% extends '@SyliusShop/Email/layout.html.twig' %}
{% block subject %}
{{ 'sylius.email.user_registration.subject'|trans({}, null, localeCode) }}
{% endblock %}
{% block content %}
{% if sylius_bundle_loaded_checker('SyliusShopBundle') %}
{% set url = channel.hostname is not null ? 'http://' ~ channel.hostname ~ path('sylius_shop_homepage', {'_locale': localeCode}) : url('sylius_shop_homepage', {'_locale': localeCode}) %}
{% endif %}
<div style="text-align: center; margin-bottom: 30px;">
<div style="font-size: 24px;">
{{ 'sylius.email.user_registration.welcome_to_our_store'|trans({}, null, localeCode) }}<br>
</div>
{{ 'sylius.email.user_registration.you_have_just_been_registered'|trans({}, null, localeCode) }} {{ user.username }}.
</div>
<div style="text-align: center;">
<a href="{{ url|raw }}" style="display: inline-block; text-align: center; background: #1abb9c; padding: 18px 28px; color: #fff; text-decoration: none; border-radius: 3px;">
{{ 'sylius.email.user_registration.start_shopping'|trans({}, null, localeCode) }}
</a>
</div>
{% endblock %}

View file

@ -1,22 +1 @@
{% extends '@SyliusShop/Email/layout.html.twig' %} {% extends '@SyliusCore/Email/userRegistration.html.twig' %}
{% block subject %}
{{ 'sylius.email.user_registration.subject'|trans({}, null, localeCode) }}
{% endblock %}
{% block content %}
{% set url = channel.hostname is not null ? 'http://' ~ channel.hostname ~ path('sylius_shop_homepage', {'_locale': localeCode}) : url('sylius_shop_homepage', {'_locale': localeCode}) %}
<div style="text-align: center; margin-bottom: 30px;">
<div style="font-size: 24px;">
{{ 'sylius.email.user_registration.welcome_to_our_store'|trans({}, null, localeCode) }}<br>
</div>
{{ 'sylius.email.user_registration.you_have_just_been_registered'|trans({}, null, localeCode) }} {{ user.username }}.
</div>
<div style="text-align: center;">
<a href="{{ url|raw }}" style="display: inline-block; text-align: center; background: #1abb9c; padding: 18px 28px; color: #fff; text-decoration: none; border-radius: 3px;">
{{ 'sylius.email.user_registration.start_shopping'|trans({}, null, localeCode) }}
</a>
</div>
{% endblock %}