wip: guest account registration (self-service and booking create flow)
This commit is contained in:
1 parent
e4430ac5c5
commit
d74888f64b
43 files changed
+2736
-144
No files matched your search
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\BpnConnect;
|
||||
|
||||
use App\BpnConnect\BpnConnectClient;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
class BpnConnectClientTest extends TestCase
|
||||
{
|
||||
public function testLookupBookingMapsResponse(): void
|
||||
{
|
||||
$capturedMethod = null;
|
||||
$capturedUrl = null;
|
||||
$capturedOptions = null;
|
||||
$httpClient = new MockHttpClient(static function (string $method, string $url, array $options) use (&$capturedMethod, &$capturedUrl, &$capturedOptions): MockResponse {
|
||||
$capturedMethod = $method;
|
||||
$capturedUrl = $url;
|
||||
$capturedOptions = $options;
|
||||
|
||||
return new MockResponse(json_encode([
|
||||
'bookingNumber' => 111386,
|
||||
'bookingId' => 98609,
|
||||
'addressId' => 225672,
|
||||
'personId' => 412344,
|
||||
'email' => '[email protected]',
|
||||
'firstName' => 'Vorname 1',
|
||||
'lastName' => 'Mustermann 1',
|
||||
], JSON_THROW_ON_ERROR), [
|
||||
'response_headers' => ['content-type' => 'application/json'],
|
||||
]);
|
||||
}, 'https://bpn-connect.test/');
|
||||
|
||||
$booking = (new BpnConnectClient($httpClient, new NullLogger()))->lookupBooking(' [email protected] ', 111386);
|
||||
|
||||
self::assertSame('GET', $capturedMethod);
|
||||
self::assertSame('https://bpn-connect.test/api/bookings/[email protected]&bookingNumber=111386', $capturedUrl);
|
||||
self::assertSame(111386, $capturedOptions['query']['bookingNumber']);
|
||||
self::assertSame(98609, $booking?->bookingId);
|
||||
self::assertSame(225672, $booking?->addressId);
|
||||
self::assertSame(412344, $booking?->personId);
|
||||
self::assertSame('[email protected]', $booking?->email);
|
||||
self::assertSame('Vorname 1', $booking?->firstName);
|
||||
self::assertSame('Mustermann 1', $booking?->lastName);
|
||||
}
|
||||
|
||||
public function testLookupBookingReturnsNullForNotFound(): void
|
||||
{
|
||||
$httpClient = new MockHttpClient(new MockResponse('', ['http_code' => 404]), 'https://bpn-connect.test/');
|
||||
|
||||
self::assertNull((new BpnConnectClient($httpClient, new NullLogger()))->lookupBooking('[email protected]', 123));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,22 @@ class BookingResponseParserTest extends TestCase
|
||||
$this->parser = new BookingResponseParser();
|
||||
}
|
||||
|
||||
public function testParseSuccessfulBookingResponse(): void
|
||||
{
|
||||
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<ergebnis>
|
||||
<satz typ="BUCHUNG" />
|
||||
<buchung>erfolgt</buchung>
|
||||
<vorgang>321530</vorgang>
|
||||
</ergebnis>';
|
||||
|
||||
$crawler = new Crawler($xmlContent);
|
||||
$response = $this->parser->parse($crawler->filter('ergebnis'));
|
||||
|
||||
self::assertTrue($response->isBookingSuccessful());
|
||||
self::assertSame(321530, $response->bookingNumber);
|
||||
}
|
||||
|
||||
public function testParseBookingResponseWithPurchaseVoucher(): void
|
||||
{
|
||||
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\CleanupGuestInviteAccountsCommand;
|
||||
use App\Repository\UserRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class CleanupGuestInviteAccountsCommandTest extends TestCase
|
||||
{
|
||||
public function testCommandReportsWhenNothingWasDeleted(): void
|
||||
{
|
||||
$repository = $this->createMock(UserRepository::class);
|
||||
$repository
|
||||
->expects(self::once())
|
||||
->method('deleteExpiredDormantGuestInvites')
|
||||
->with(self::isInstanceOf(\DateTimeImmutable::class))
|
||||
->willReturn(0);
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects(self::never())->method('info');
|
||||
|
||||
$tester = new CommandTester(new CleanupGuestInviteAccountsCommand($repository, $logger));
|
||||
$exitCode = $tester->execute([]);
|
||||
|
||||
self::assertSame(0, $exitCode);
|
||||
self::assertStringContainsString('No expired dormant guest accounts found.', $tester->getDisplay());
|
||||
}
|
||||
|
||||
public function testCommandLogsAndReportsDeletionCount(): void
|
||||
{
|
||||
$repository = $this->createMock(UserRepository::class);
|
||||
$repository
|
||||
->expects(self::once())
|
||||
->method('deleteExpiredDormantGuestInvites')
|
||||
->with(self::isInstanceOf(\DateTimeImmutable::class))
|
||||
->willReturn(3);
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger
|
||||
->expects(self::once())
|
||||
->method('info')
|
||||
->with(
|
||||
'Deleted expired dormant guest accounts',
|
||||
self::callback(static fn (array $context): bool => 3 === $context['count'])
|
||||
);
|
||||
|
||||
$tester = new CommandTester(new CleanupGuestInviteAccountsCommand($repository, $logger));
|
||||
$exitCode = $tester->execute([]);
|
||||
|
||||
self::assertSame(0, $exitCode);
|
||||
self::assertStringContainsString('Deleted 3 expired dormant guest account(s).', $tester->getDisplay());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller\Account;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Exception\GuestAccountActivationExpiredException;
|
||||
use App\Service\BookingGuestAccountManager;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class GuestAccountActivationControllerTest extends WebTestCase
|
||||
{
|
||||
public function testExpiredActivationLinkShowsResendPageInsteadOf404(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('validateActivationRequest')
|
||||
->with(self::isInstanceOf(Request::class))
|
||||
->willThrowException(new GuestAccountActivationExpiredException());
|
||||
$guestAccountManager->expects(self::never())->method('consumeActivation');
|
||||
$guestAccountManager->expects(self::never())->method('validateActivationUrlForResend');
|
||||
$guestAccountManager->expects(self::never())->method('resendWelcomeEmail');
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects(self::never())->method('login');
|
||||
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
self::getContainer()->set(Security::class, $security);
|
||||
|
||||
$client->request('GET', '/welcome/[email protected]&nonce=current-nonce&expires=9999999999&_hash=signature');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertStringContainsString('Link', $client->getResponse()->getContent());
|
||||
self::assertStringContainsString('Willkommens-E-Mail erneut senden', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testResendActionResendsWelcomeMailAndRedirectsToLogin(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$user = new User('[email protected]');
|
||||
$user->setWelcomeActivationNonce('current-nonce');
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('validateActivationRequest')
|
||||
->with(self::isInstanceOf(Request::class))
|
||||
->willThrowException(new GuestAccountActivationExpiredException());
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('validateActivationUrlForResend')
|
||||
->with(self::callback(static function (string $activationUrl): bool {
|
||||
return str_contains($activationUrl, 'email=customer%40example.com')
|
||||
|| str_contains($activationUrl, '[email protected]');
|
||||
}))
|
||||
->willReturn($user);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('resendWelcomeEmail')
|
||||
->with($user);
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects(self::never())->method('login');
|
||||
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
self::getContainer()->set(Security::class, $security);
|
||||
|
||||
$crawler = $client->request('GET', '/welcome/[email protected]&nonce=current-nonce&expires=9999999999&_hash=signature');
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('Willkommens-E-Mail erneut senden')->form();
|
||||
$client->submit($form);
|
||||
|
||||
self::assertResponseRedirects('/');
|
||||
$client->followRedirect();
|
||||
|
||||
self::assertStringContainsString('Wir haben dir die Willkommens-E-Mail erneut gesendet.', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testTamperedActivationLinkStillReturns404(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('validateActivationRequest')
|
||||
->with(self::isInstanceOf(Request::class))
|
||||
->willThrowException(new NotFoundHttpException());
|
||||
$guestAccountManager->expects(self::never())->method('consumeActivation');
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects(self::never())->method('login');
|
||||
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
self::getContainer()->set(Security::class, $security);
|
||||
|
||||
$client->request('GET', '/welcome/[email protected]&nonce=current-nonce&expires=9999999999&_hash=invalid');
|
||||
|
||||
self::assertResponseStatusCodeSame(404);
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,59 @@ class PersonalDataControllerTest extends TestCase
|
||||
], $controller->flashes);
|
||||
}
|
||||
|
||||
public function testIndexConsumesWelcomeActivationAfterForcedPasswordChangeSucceeds(): void
|
||||
{
|
||||
$personalData = $this->createCompletePersonalData();
|
||||
$controller = $this->createController(true, true, $this->validator(), 'NewPassw0rd!');
|
||||
$controller->user->markWelcomeActivationPending('current-nonce', new \DateTimeImmutable('+30 minutes'));
|
||||
|
||||
$controller->apiClient
|
||||
->expects(self::once())
|
||||
->method('getPersonalData')
|
||||
->with('[email protected]', null, null)
|
||||
->willReturn($personalData);
|
||||
$controller->apiClient
|
||||
->expects(self::once())
|
||||
->method('updatePersonalData')
|
||||
->with('[email protected]', $personalData)
|
||||
->willReturn($personalData);
|
||||
$controller->newsletterManager
|
||||
->expects(self::once())
|
||||
->method('hasConfirmedOptIn')
|
||||
->with('[email protected]')
|
||||
->willReturn(false);
|
||||
$controller->newsletterManager
|
||||
->expects(self::once())
|
||||
->method('hasPendingConfirmation')
|
||||
->with('[email protected]')
|
||||
->willReturn(false);
|
||||
$controller->dataLoader
|
||||
->expects(self::once())
|
||||
->method('invalidateUserBookingCaches')
|
||||
->with($controller->user);
|
||||
$controller->entityManager
|
||||
->expects(self::once())
|
||||
->method('flush');
|
||||
$controller->passwordHasher
|
||||
->expects(self::once())
|
||||
->method('hashPassword')
|
||||
->with($controller->user, 'NewPassw0rd!')
|
||||
->willReturn('new-local-hash');
|
||||
|
||||
$request = Request::create('/personal-data', 'POST');
|
||||
$request->setSession(new Session(new MockArraySessionStorage()));
|
||||
$request->getSession()->set(PersonalDataController::SESSION_FORCE_PASSWORD_CHANGE_KEY, true);
|
||||
|
||||
$response = $controller->index($request);
|
||||
|
||||
self::assertSame(302, $response->getStatusCode());
|
||||
self::assertSame('new-local-hash', $controller->user->getPassword());
|
||||
self::assertNull($controller->user->getWelcomeActivationNonce());
|
||||
self::assertNull($controller->user->getWelcomeActivationExpiresAt());
|
||||
self::assertNotNull($controller->user->getWelcomeActivatedAt());
|
||||
self::assertFalse($request->getSession()->has(PersonalDataController::SESSION_FORCE_PASSWORD_CHANGE_KEY));
|
||||
}
|
||||
|
||||
public function testIndexDoesNotPersistInvalidFormSubmission(): void
|
||||
{
|
||||
$controller = $this->createController(true, false, $this->validator(), 'NewPassw0rd!');
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller\Account;
|
||||
|
||||
use App\Service\BookingGuestAccountManager;
|
||||
use App\Service\PublicFormRateLimiter;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class PortalAccessRequestControllerTest extends WebTestCase
|
||||
{
|
||||
public function testPortalAccessRequestRedirectsToLoginAfterSendingActivationMail(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$rateLimiter = $this->createMock(PublicFormRateLimiter::class);
|
||||
$rateLimiter
|
||||
->expects(self::once())
|
||||
->method('isPortalAccessAllowed')
|
||||
->with('[email protected]', self::isInstanceOf(Request::class))
|
||||
->willReturn(true);
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('requestPortalAccess')
|
||||
->with('[email protected]', 123)
|
||||
->willReturn(BookingGuestAccountManager::PORTAL_ACCESS_SENT);
|
||||
|
||||
self::getContainer()->set(PublicFormRateLimiter::class, $rateLimiter);
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
|
||||
$crawler = $client->request('GET', '/');
|
||||
$token = $crawler->filter('input[name="_token"]')->attr('value');
|
||||
$client->request('POST', '/portal-access/request', [
|
||||
'email' => '[email protected]',
|
||||
'bookingNumber' => '123',
|
||||
'_token' => $token,
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/');
|
||||
$client->followRedirect();
|
||||
|
||||
self::assertStringContainsString('Wenn zu deiner Buchung ein Portalzugang möglich ist, haben wir dir eine E-Mail gesendet.', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testPortalAccessRequestRejectsMismatchedBookingGenerically(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$rateLimiter = $this->createMock(PublicFormRateLimiter::class);
|
||||
$rateLimiter
|
||||
->expects(self::once())
|
||||
->method('isPortalAccessAllowed')
|
||||
->with('[email protected]', self::isInstanceOf(Request::class))
|
||||
->willReturn(true);
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('requestPortalAccess')
|
||||
->with('[email protected]', 123)
|
||||
->willReturn(BookingGuestAccountManager::PORTAL_ACCESS_NOT_SENT);
|
||||
|
||||
self::getContainer()->set(PublicFormRateLimiter::class, $rateLimiter);
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
|
||||
$crawler = $client->request('GET', '/');
|
||||
$token = $crawler->filter('input[name="_token"]')->attr('value');
|
||||
$client->request('POST', '/portal-access/request', [
|
||||
'email' => '[email protected]',
|
||||
'bookingNumber' => '123',
|
||||
'_token' => $token,
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/');
|
||||
$client->followRedirect();
|
||||
|
||||
self::assertStringContainsString('Wenn zu deiner Buchung ein Portalzugang möglich ist, haben wir dir eine E-Mail gesendet.', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testPortalAccessRequestShowsLoginHintForAlreadyActiveAccount(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$rateLimiter = $this->createMock(PublicFormRateLimiter::class);
|
||||
$rateLimiter
|
||||
->expects(self::once())
|
||||
->method('isPortalAccessAllowed')
|
||||
->with('[email protected]', self::isInstanceOf(Request::class))
|
||||
->willReturn(true);
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('requestPortalAccess')
|
||||
->with('[email protected]', 123)
|
||||
->willReturn(BookingGuestAccountManager::PORTAL_ACCESS_ALREADY_ACTIVE);
|
||||
|
||||
self::getContainer()->set(PublicFormRateLimiter::class, $rateLimiter);
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
|
||||
$crawler = $client->request('GET', '/');
|
||||
$token = $crawler->filter('input[name="_token"]')->attr('value');
|
||||
$client->request('POST', '/portal-access/request', [
|
||||
'email' => '[email protected]',
|
||||
'bookingNumber' => '123',
|
||||
'_token' => $token,
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/');
|
||||
$client->followRedirect();
|
||||
|
||||
self::assertStringContainsString('Für diese E-Mail-Adresse besteht bereits ein aktiver Portalzugang.', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testPortalAccessRequestReturns429WhenThrottled(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$rateLimiter = $this->createMock(PublicFormRateLimiter::class);
|
||||
$rateLimiter
|
||||
->expects(self::once())
|
||||
->method('isPortalAccessAllowed')
|
||||
->with('[email protected]', self::isInstanceOf(Request::class))
|
||||
->willReturn(false);
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager->expects(self::never())->method('requestPortalAccess');
|
||||
|
||||
self::getContainer()->set(PublicFormRateLimiter::class, $rateLimiter);
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
|
||||
$crawler = $client->request('GET', '/');
|
||||
$token = $crawler->filter('input[name="_token"]')->attr('value');
|
||||
$client->request('POST', '/portal-access/request', [
|
||||
'email' => '[email protected]',
|
||||
'bookingNumber' => '123',
|
||||
'_token' => $token,
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(429);
|
||||
self::assertStringContainsString(PublicFormRateLimiter::THROTTLED_MESSAGE, $client->getResponse()->getContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller\Booking\Create;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\BookingResponse;
|
||||
use App\BusProNet\Model\Hotel;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\BookingCreateContext;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\BookingSummaryDto;
|
||||
use App\Form\Model\BookingSummaryPricingDto;
|
||||
use App\Form\Model\BookingSummaryVoucherDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\RoomGroupsDto;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Service\BookingConfigurator;
|
||||
use App\Service\BookingCreateContextFactory;
|
||||
use App\Service\BookingGuestAccountManager;
|
||||
use App\Service\BookingSessionManager;
|
||||
use App\Service\NewsletterManager;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
class Step4ControllerTest extends WebTestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
self::ensureKernelShutdown();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testStep4RendersCheckboxAndProvisionsGuestAccountAfterSuccessfulBooking(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$context = $this->createBookingContext($bookingDto);
|
||||
|
||||
$bookingSessionManager = $this->createMock(BookingSessionManager::class);
|
||||
$bookingSessionManager
|
||||
->expects(self::exactly(2))
|
||||
->method('getOrCreateBookingCreateDto')
|
||||
->willReturn($bookingDto);
|
||||
|
||||
$contextFactory = $this->createMock(BookingCreateContextFactory::class);
|
||||
$contextFactory
|
||||
->expects(self::exactly(2))
|
||||
->method('createWithParticipantPrices')
|
||||
->with($bookingDto)
|
||||
->willReturn($context);
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::exactly(2))
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn(null);
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('provisionGuestAccountForBooking')
|
||||
->with('[email protected]', 123, 'Alice', 'Example')
|
||||
->willReturn(true);
|
||||
|
||||
$bookingConfigurator = $this->createMock(BookingConfigurator::class);
|
||||
$bookingConfigurator
|
||||
->expects(self::once())
|
||||
->method('applyCreateBookingStatusRules');
|
||||
|
||||
$apiClient = $this->createMock(ApiClient::class);
|
||||
$apiClient
|
||||
->expects(self::once())
|
||||
->method('createBooking')
|
||||
->with($bookingDto)
|
||||
->willReturn(new BookingResponse(BookingResponse::BOOKING_PROCESSED, 123));
|
||||
|
||||
$newsletterManager = $this->createMock(NewsletterManager::class);
|
||||
$newsletterManager->expects(self::never())->method('requestConfirmation');
|
||||
|
||||
self::getContainer()->set(BookingSessionManager::class, $bookingSessionManager);
|
||||
self::getContainer()->set(BookingCreateContextFactory::class, $contextFactory);
|
||||
self::getContainer()->set(UserRepository::class, $userRepository);
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
self::getContainer()->set(BookingConfigurator::class, $bookingConfigurator);
|
||||
self::getContainer()->set(ApiClient::class, $apiClient);
|
||||
self::getContainer()->set(NewsletterManager::class, $newsletterManager);
|
||||
|
||||
$crawler = $client->request('GET', '/bookings/create/confirmation');
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertStringContainsString('Benutzeraccount in MyE&P anlegen', $client->getResponse()->getContent());
|
||||
|
||||
$form = $crawler->selectButton('Verbindlich buchen')->form([
|
||||
'booking_create_step4[confirmationAccepted]' => '1',
|
||||
'booking_create_step4[termsAccepted]' => '1',
|
||||
'booking_create_step4[guestAccountOptIn]' => '1',
|
||||
]);
|
||||
$client->submit($form);
|
||||
|
||||
self::assertResponseRedirects('/bookings/create/success');
|
||||
}
|
||||
|
||||
public function testStep4HidesGuestAccountCheckboxForAuthenticatedUsers(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository->expects(self::never())->method('findOneByNormalizedEmail');
|
||||
|
||||
$controller = $this->createAuthenticatedController(
|
||||
user: new User('[email protected]'),
|
||||
userRepository: $userRepository,
|
||||
);
|
||||
|
||||
self::assertNull($controller->exposeGuestAccountTargetEmail($bookingDto));
|
||||
}
|
||||
|
||||
public function testStep4HidesGuestAccountCheckboxWhenLocalUserAlreadyExists(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$context = $this->createBookingContext($bookingDto);
|
||||
|
||||
$bookingSessionManager = $this->createMock(BookingSessionManager::class);
|
||||
$bookingSessionManager
|
||||
->expects(self::once())
|
||||
->method('getOrCreateBookingCreateDto')
|
||||
->willReturn($bookingDto);
|
||||
|
||||
$contextFactory = $this->createMock(BookingCreateContextFactory::class);
|
||||
$contextFactory
|
||||
->expects(self::once())
|
||||
->method('createWithParticipantPrices')
|
||||
->with($bookingDto)
|
||||
->willReturn($context);
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::once())
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn(new User('[email protected]'));
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager->expects(self::never())->method('provisionGuestAccount');
|
||||
|
||||
self::getContainer()->set(BookingSessionManager::class, $bookingSessionManager);
|
||||
self::getContainer()->set(BookingCreateContextFactory::class, $contextFactory);
|
||||
self::getContainer()->set(UserRepository::class, $userRepository);
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
|
||||
$client->request('GET', '/bookings/create/confirmation');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertStringNotContainsString('Benutzeraccount in MyE&P anlegen', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testGuestAccountActivationRedirectsToPersonalDataAndForcesPasswordChange(): void
|
||||
{
|
||||
$client = self::createClient([], ['HTTPS' => 'on']);
|
||||
$client->disableReboot();
|
||||
|
||||
$guestAccountManager = $this->createMock(BookingGuestAccountManager::class);
|
||||
$guestAccountManager
|
||||
->expects(self::once())
|
||||
->method('validateActivationRequest')
|
||||
->with(self::isInstanceOf(\Symfony\Component\HttpFoundation\Request::class))
|
||||
->willReturn(new User('[email protected]'));
|
||||
$guestAccountManager
|
||||
->expects(self::never())
|
||||
->method('consumeActivation');
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security
|
||||
->expects(self::once())
|
||||
->method('login')
|
||||
->with(self::isInstanceOf(User::class), null, 'main')
|
||||
->willReturn(null);
|
||||
|
||||
self::getContainer()->set(BookingGuestAccountManager::class, $guestAccountManager);
|
||||
self::getContainer()->set(Security::class, $security);
|
||||
|
||||
$client->request('GET', '/welcome/[email protected]&nonce=current-nonce&expires=9999999999&_hash=signature');
|
||||
|
||||
self::assertResponseRedirects('/personal-data');
|
||||
self::assertTrue($client->getRequest()->getSession()->get(\App\Controller\Account\PersonalDataController::SESSION_FORCE_PASSWORD_CHANGE_KEY));
|
||||
}
|
||||
|
||||
private function createBookingDto(): BookingDto
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->id = 123;
|
||||
$travel->label = 'Winterreise';
|
||||
$travel->productCode = 'PROD-1';
|
||||
$travel->dateFrom = new \DateTimeImmutable('2026-01-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2026-01-08');
|
||||
$hotel = new Hotel();
|
||||
$hotel->name = 'Hotel Test';
|
||||
$travel->hotel = $hotel;
|
||||
|
||||
$bookingDto = new BookingDto($travel, 77);
|
||||
$bookingDto->currentStep = 4;
|
||||
$bookingDto->bookingStatus = 'F';
|
||||
$bookingDto->paymentMethod = Constants::PAYMENT_METHOD_TRANSFER;
|
||||
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 0;
|
||||
$participant->firstName = 'Alice';
|
||||
$participant->lastName = 'Example';
|
||||
$participant->email = '[email protected]';
|
||||
$participant->gender = 'M';
|
||||
$participant->nationality = 'DE';
|
||||
$participant->dateOfBirth = new \DateTimeImmutable('1990-01-01');
|
||||
$bookingDto->participants = [$participant];
|
||||
|
||||
return $bookingDto;
|
||||
}
|
||||
|
||||
private function createBookingContext(BookingDto $bookingDto): BookingCreateContext
|
||||
{
|
||||
$pricing = new BookingSummaryPricingDto([], [], null, [], 0.0);
|
||||
$vouchers = new BookingSummaryVoucherDto(null, 0.0);
|
||||
$summary = new BookingSummaryDto([], 1, $pricing, $vouchers, null);
|
||||
|
||||
return new BookingCreateContext(
|
||||
bookingDto: $bookingDto,
|
||||
summaryData: $summary,
|
||||
groupedRooms: new RoomGroupsDto([], []),
|
||||
participantPrices: [0.0],
|
||||
);
|
||||
}
|
||||
|
||||
private function createAuthenticatedController(User $user, UserRepository $userRepository): TestableStep4Controller
|
||||
{
|
||||
return new TestableStep4Controller(
|
||||
$this->createMock(BookingConfigurator::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$this->createMock(BookingCreateContextFactory::class),
|
||||
$this->createMock(BookingGuestAccountManager::class),
|
||||
$this->createMock(ApiClient::class),
|
||||
$this->createMock(\Symfony\Contracts\Cache\CacheInterface::class),
|
||||
$this->createMock(NewsletterManager::class),
|
||||
$userRepository,
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$user,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class TestableStep4Controller extends \App\Controller\Booking\Create\Step4Controller
|
||||
{
|
||||
public function __construct(
|
||||
BookingConfigurator $bookingService,
|
||||
BookingSessionManager $bookingSessionService,
|
||||
BookingCreateContextFactory $createContextFactory,
|
||||
BookingGuestAccountManager $guestAccountManager,
|
||||
ApiClient $apiClient,
|
||||
\Symfony\Contracts\Cache\CacheInterface $cache,
|
||||
NewsletterManager $doubleOptInService,
|
||||
UserRepository $userRepository,
|
||||
\Psr\Log\LoggerInterface $logger,
|
||||
private readonly User $user,
|
||||
) {
|
||||
parent::__construct(
|
||||
$bookingService,
|
||||
$bookingSessionService,
|
||||
$createContextFactory,
|
||||
$guestAccountManager,
|
||||
$apiClient,
|
||||
$cache,
|
||||
$doubleOptInService,
|
||||
$userRepository,
|
||||
$logger,
|
||||
);
|
||||
}
|
||||
|
||||
public function exposeGuestAccountTargetEmail(BookingDto $bookingDto): ?string
|
||||
{
|
||||
return $this->resolveGuestAccountTargetEmail($bookingDto);
|
||||
}
|
||||
|
||||
protected function getUser(): \Symfony\Component\Security\Core\User\UserInterface|null
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\EventListener;
|
||||
|
||||
use App\Controller\Account\PersonalDataController;
|
||||
use App\Entity\User;
|
||||
use App\EventListener\ForcePasswordChangeRedirectListener;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class ForcePasswordChangeRedirectListenerTest extends TestCase
|
||||
{
|
||||
private HttpKernelInterface $kernel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->kernel = $this->createMock(HttpKernelInterface::class);
|
||||
}
|
||||
|
||||
public function testRedirectsAwayFromNonPersonalDataRoutesWhenPasswordChangeIsForced(): void
|
||||
{
|
||||
$listener = $this->createListener();
|
||||
$request = $this->createRequest('/account', 'app_account');
|
||||
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$listener->onKernelRequest($event);
|
||||
|
||||
self::assertInstanceOf(RedirectResponse::class, $event->getResponse());
|
||||
self::assertSame('/personal-data', $event->getResponse()->headers->get('Location'));
|
||||
}
|
||||
|
||||
public function testAllowsPersonalDataRouteWhilePasswordChangeIsForced(): void
|
||||
{
|
||||
$listener = $this->createListener();
|
||||
$request = $this->createRequest('/personal-data', 'app_personal_data');
|
||||
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$listener->onKernelRequest($event);
|
||||
|
||||
self::assertNull($event->getResponse());
|
||||
}
|
||||
|
||||
public function testNewsletterSubRouteIsNoLongerExempt(): void
|
||||
{
|
||||
$listener = $this->createListener();
|
||||
$request = $this->createRequest('/personal-data/newsletter', 'app_personal_data_newsletter', ['HX-Request' => 'true']);
|
||||
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$listener->onKernelRequest($event);
|
||||
|
||||
self::assertInstanceOf(HxRedirectResponse::class, $event->getResponse());
|
||||
self::assertSame('/personal-data', $event->getResponse()->headers->get('HX-Redirect'));
|
||||
}
|
||||
|
||||
public function testLogoutRouteRemainsReachable(): void
|
||||
{
|
||||
$listener = $this->createListener();
|
||||
$request = $this->createRequest('/logout', 'app_logout');
|
||||
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$listener->onKernelRequest($event);
|
||||
|
||||
self::assertNull($event->getResponse());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $headers
|
||||
*/
|
||||
private function createRequest(string $uri, string $route, array $headers = []): Request
|
||||
{
|
||||
$server = [];
|
||||
foreach ($headers as $header => $value) {
|
||||
$server['HTTP_'.strtoupper(str_replace('-', '_', $header))] = $value;
|
||||
}
|
||||
|
||||
$request = Request::create($uri, 'GET', [], [], [], $server);
|
||||
$request->setSession(new Session(new MockArraySessionStorage()));
|
||||
$request->getSession()->set(PersonalDataController::SESSION_FORCE_PASSWORD_CHANGE_KEY, true);
|
||||
$request->attributes->set('_route', $route);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
private function createListener(): ForcePasswordChangeRedirectListener
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$security
|
||||
->method('getUser')
|
||||
->willReturn(new User('[email protected]'));
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator
|
||||
->method('generate')
|
||||
->with('app_personal_data')
|
||||
->willReturn('/personal-data');
|
||||
|
||||
return new ForcePasswordChangeRedirectListener($security, $urlGenerator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserRepositoryTest extends TestCase
|
||||
{
|
||||
public function testDeleteExpiredDormantGuestInvitesBuildsExpectedQuery(): void
|
||||
{
|
||||
$query = $this->createMock(Query::class);
|
||||
$query
|
||||
->expects(self::once())
|
||||
->method('execute')
|
||||
->willReturn(2);
|
||||
|
||||
$threshold = new \DateTimeImmutable('2026-05-29 01:00:00');
|
||||
$queryBuilder = $this->createMock(QueryBuilder::class);
|
||||
$andWhereCalls = 0;
|
||||
$queryBuilder
|
||||
->expects(self::once())
|
||||
->method('delete')
|
||||
->willReturnSelf();
|
||||
$queryBuilder
|
||||
->expects(self::once())
|
||||
->method('where')
|
||||
->with('u.welcomeActivatedAt IS NULL')
|
||||
->willReturnSelf();
|
||||
$queryBuilder
|
||||
->expects(self::exactly(2))
|
||||
->method('andWhere')
|
||||
->willReturnCallback(function (string $condition) use (&$andWhereCalls, $queryBuilder): QueryBuilder {
|
||||
$expectedConditions = [
|
||||
'u.welcomeActivationExpiresAt IS NOT NULL',
|
||||
'u.welcomeActivationExpiresAt <= :threshold',
|
||||
];
|
||||
|
||||
self::assertSame($expectedConditions[$andWhereCalls], $condition);
|
||||
++$andWhereCalls;
|
||||
|
||||
return $queryBuilder;
|
||||
});
|
||||
$queryBuilder
|
||||
->expects(self::once())
|
||||
->method('setParameter')
|
||||
->with('threshold', $threshold)
|
||||
->willReturnSelf();
|
||||
$queryBuilder
|
||||
->expects(self::once())
|
||||
->method('getQuery')
|
||||
->willReturn($query);
|
||||
|
||||
$repository = new class($queryBuilder) extends UserRepository {
|
||||
public function __construct(private readonly QueryBuilder $queryBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
public function createQueryBuilder($alias, $indexBy = null): QueryBuilder
|
||||
{
|
||||
return $this->queryBuilder;
|
||||
}
|
||||
};
|
||||
|
||||
self::assertSame(2, $repository->deleteExpiredDormantGuestInvites($threshold));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\BpnConnect\BpnConnectClient;
|
||||
use App\BpnConnect\Model\BookingLookupResult;
|
||||
use App\Email\Mailer;
|
||||
use App\Entity\User;
|
||||
use App\Exception\GuestAccountActivationExpiredException;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Service\BookingGuestAccountManager;
|
||||
use App\Service\UserBootstrapper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\UriSigner;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class BookingGuestAccountManagerTest extends TestCase
|
||||
{
|
||||
public function testProvisionGuestAccountCreatesCustomerAndSendsActivationMail(): void
|
||||
{
|
||||
$persistedUser = null;
|
||||
$plainPassword = null;
|
||||
$generatedRoute = null;
|
||||
$generatedParameters = null;
|
||||
$generatedReferenceType = null;
|
||||
$capturedContext = null;
|
||||
$capturedOptions = null;
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::once())
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn(null);
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager
|
||||
->expects(self::once())
|
||||
->method('persist')
|
||||
->with(self::callback(static function (User $user) use (&$persistedUser): bool {
|
||||
$persistedUser = $user;
|
||||
|
||||
return true;
|
||||
}));
|
||||
$entityManager
|
||||
->expects(self::once())
|
||||
->method('flush');
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$passwordHasher
|
||||
->expects(self::once())
|
||||
->method('hashPassword')
|
||||
->with(self::isInstanceOf(User::class), self::callback(static function (string $password) use (&$plainPassword): bool {
|
||||
$plainPassword = $password;
|
||||
|
||||
return '' !== $password;
|
||||
}))
|
||||
->willReturn('hashed-password');
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator
|
||||
->expects(self::once())
|
||||
->method('generate')
|
||||
->willReturnCallback(static function (string $route, array $parameters, int $referenceType) use (&$generatedRoute, &$generatedParameters, &$generatedReferenceType): string {
|
||||
$generatedRoute = $route;
|
||||
$generatedParameters = $parameters;
|
||||
$generatedReferenceType = $referenceType;
|
||||
|
||||
return 'https://example.test/welcome/activate?'.http_build_query($parameters);
|
||||
});
|
||||
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$mailer
|
||||
->expects(self::once())
|
||||
->method('createAndSendEmail')
|
||||
->willReturnCallback(static function (array $context, array $options) use (&$capturedContext, &$capturedOptions): void {
|
||||
$capturedContext = $context;
|
||||
$capturedOptions = $options;
|
||||
});
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer);
|
||||
$uriSigner = new UriSigner('test-secret');
|
||||
|
||||
self::assertTrue($manager->provisionGuestAccount(' [email protected] ', 'Alice', 'Example', 12345, 67890));
|
||||
self::assertSame('app_guest_account_activate', $generatedRoute);
|
||||
self::assertSame(UrlGeneratorInterface::ABSOLUTE_URL, $generatedReferenceType);
|
||||
self::assertSame('[email protected]', $generatedParameters['email']);
|
||||
self::assertSame($persistedUser?->getWelcomeActivationNonce(), $generatedParameters['nonce']);
|
||||
self::assertTrue($uriSigner->check((string) $capturedContext['activationUrl']));
|
||||
self::assertSame('[email protected]', $capturedOptions['to']);
|
||||
self::assertSame('Dein MyE&P Konto ist bereit', $capturedOptions['subject']);
|
||||
self::assertSame('email/booking_guest_account_welcome.html.twig', $capturedOptions['template']);
|
||||
self::assertSame('Alice', $capturedContext['firstName']);
|
||||
self::assertSame('Example', $capturedContext['lastName']);
|
||||
self::assertMatchesRegularExpression('/^[a-f0-9]{48}$/', (string) $plainPassword);
|
||||
self::assertInstanceOf(User::class, $persistedUser);
|
||||
self::assertSame('[email protected]', $persistedUser->getEmail());
|
||||
self::assertSame(12345, $persistedUser->getAddressId());
|
||||
self::assertSame(67890, $persistedUser->getPersonId());
|
||||
self::assertSame(['ROLE_USER', 'ROLE_CUSTOMER'], $persistedUser->getRoles());
|
||||
self::assertSame([], $persistedUser->getHotelCodes());
|
||||
self::assertTrue($persistedUser->isProfileComplete());
|
||||
self::assertMatchesRegularExpression('/^[a-f0-9]{64}$/', (string) $persistedUser->getWelcomeActivationNonce());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $persistedUser->getWelcomeActivationExpiresAt());
|
||||
self::assertNull($persistedUser->getWelcomeActivatedAt());
|
||||
}
|
||||
|
||||
public function testProvisionGuestAccountReturnsFalseWhenLocalUserAlreadyExists(): void
|
||||
{
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::once())
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn(new User('[email protected]'));
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager->expects(self::never())->method('persist');
|
||||
$entityManager->expects(self::never())->method('flush');
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$passwordHasher->expects(self::never())->method('hashPassword');
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->expects(self::never())->method('generate');
|
||||
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$mailer->expects(self::never())->method('createAndSendEmail');
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer);
|
||||
|
||||
self::assertFalse($manager->provisionGuestAccount('[email protected]', 'Alice', 'Example'));
|
||||
}
|
||||
|
||||
public function testRequestPortalAccessProvisionsGuestAccountWhenBookingMatches(): void
|
||||
{
|
||||
$persistedUser = null;
|
||||
$capturedContext = null;
|
||||
$capturedOptions = null;
|
||||
$booking = $this->createLookupResult('[email protected]', 123, 'Alice', 'Example');
|
||||
|
||||
$bpnConnectClient = $this->createMock(BpnConnectClient::class);
|
||||
$bpnConnectClient
|
||||
->expects(self::once())
|
||||
->method('lookupBooking')
|
||||
->with('[email protected]', 123)
|
||||
->willReturn($booking);
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::exactly(2))
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturnOnConsecutiveCalls(null, null);
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager
|
||||
->expects(self::once())
|
||||
->method('persist')
|
||||
->with(self::callback(static function (User $user) use (&$persistedUser): bool {
|
||||
$persistedUser = $user;
|
||||
|
||||
return true;
|
||||
}));
|
||||
$entityManager->expects(self::once())->method('flush');
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$passwordHasher
|
||||
->expects(self::once())
|
||||
->method('hashPassword')
|
||||
->willReturn('hashed-password');
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator
|
||||
->expects(self::once())
|
||||
->method('generate')
|
||||
->willReturn('https://example.test/welcome/[email protected]&nonce=current&expires=9999999999');
|
||||
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$mailer
|
||||
->expects(self::once())
|
||||
->method('createAndSendEmail')
|
||||
->willReturnCallback(static function (array $context, array $options) use (&$capturedContext, &$capturedOptions): void {
|
||||
$capturedContext = $context;
|
||||
$capturedOptions = $options;
|
||||
});
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer, $bpnConnectClient);
|
||||
|
||||
self::assertSame(BookingGuestAccountManager::PORTAL_ACCESS_SENT, $manager->requestPortalAccess('[email protected]', 123));
|
||||
self::assertInstanceOf(User::class, $persistedUser);
|
||||
self::assertSame('[email protected]', $persistedUser->getEmail());
|
||||
self::assertSame(12345, $persistedUser->getAddressId());
|
||||
self::assertSame(67890, $persistedUser->getPersonId());
|
||||
self::assertSame('[email protected]', $capturedOptions['to']);
|
||||
self::assertSame('Alice', $capturedContext['firstName']);
|
||||
self::assertSame('Example', $capturedContext['lastName']);
|
||||
self::assertMatchesRegularExpression('/^[a-f0-9]{64}$/', (string) $persistedUser->getWelcomeActivationNonce());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $persistedUser->getWelcomeActivationExpiresAt());
|
||||
self::assertNull($persistedUser->getWelcomeActivatedAt());
|
||||
}
|
||||
|
||||
public function testRequestPortalAccessResendsPendingInviteWhenBookingMatches(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
$user->setAddressId(12345);
|
||||
$user->setPersonId(67890);
|
||||
$user->setWelcomeActivationNonce('current-nonce');
|
||||
$user->setWelcomeActivationExpiresAt(new \DateTimeImmutable('+30 minutes'));
|
||||
$booking = $this->createLookupResult('[email protected]', 123, 'Alice', 'Example');
|
||||
|
||||
$bpnConnectClient = $this->createMock(BpnConnectClient::class);
|
||||
$bpnConnectClient
|
||||
->expects(self::once())
|
||||
->method('lookupBooking')
|
||||
->with('[email protected]', 123)
|
||||
->willReturn($booking);
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::once())
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn($user);
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager->expects(self::once())->method('flush');
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$passwordHasher->expects(self::never())->method('hashPassword');
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator
|
||||
->expects(self::once())
|
||||
->method('generate')
|
||||
->willReturn('https://example.test/welcome/[email protected]&nonce=new-nonce&expires=9999999999');
|
||||
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$mailer
|
||||
->expects(self::once())
|
||||
->method('createAndSendEmail');
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer, $bpnConnectClient);
|
||||
|
||||
self::assertSame(BookingGuestAccountManager::PORTAL_ACCESS_SENT, $manager->requestPortalAccess('[email protected]', 123));
|
||||
self::assertNotSame('current-nonce', $user->getWelcomeActivationNonce());
|
||||
self::assertNull($user->getWelcomeActivatedAt());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $user->getWelcomeActivationExpiresAt());
|
||||
}
|
||||
|
||||
public function testRequestPortalAccessUsesExistingUserIdsAndDoesNotMailActivatedAccount(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
$user->setAddressId(12345);
|
||||
$user->setPersonId(67890);
|
||||
$user->markWelcomeActivated();
|
||||
$booking = $this->createLookupResult('[email protected]', 123, 'Alice', 'Example');
|
||||
|
||||
$bpnConnectClient = $this->createMock(BpnConnectClient::class);
|
||||
$bpnConnectClient
|
||||
->expects(self::once())
|
||||
->method('lookupBooking')
|
||||
->with('[email protected]', 123)
|
||||
->willReturn($booking);
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::once())
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn($user);
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager->expects(self::never())->method('persist');
|
||||
$entityManager->expects(self::never())->method('flush');
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$passwordHasher->expects(self::never())->method('hashPassword');
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->expects(self::never())->method('generate');
|
||||
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$mailer->expects(self::never())->method('createAndSendEmail');
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer, $bpnConnectClient);
|
||||
|
||||
self::assertSame(BookingGuestAccountManager::PORTAL_ACCESS_ALREADY_ACTIVE, $manager->requestPortalAccess('[email protected]', 123));
|
||||
}
|
||||
|
||||
public function testProvisionGuestAccountForBookingFetchesBookingIdentifiers(): void
|
||||
{
|
||||
$persistedUser = null;
|
||||
$booking = $this->createLookupResult('[email protected]', 123, 'Alice', 'Example');
|
||||
|
||||
$bpnConnectClient = $this->createMock(BpnConnectClient::class);
|
||||
$bpnConnectClient
|
||||
->expects(self::once())
|
||||
->method('lookupBooking')
|
||||
->with('[email protected]', 123)
|
||||
->willReturn($booking);
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::once())
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn(null);
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager
|
||||
->expects(self::once())
|
||||
->method('persist')
|
||||
->with(self::callback(static function (User $user) use (&$persistedUser): bool {
|
||||
$persistedUser = $user;
|
||||
|
||||
return true;
|
||||
}));
|
||||
$entityManager->expects(self::once())->method('flush');
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$passwordHasher
|
||||
->expects(self::once())
|
||||
->method('hashPassword')
|
||||
->willReturn('hashed-password');
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator
|
||||
->expects(self::once())
|
||||
->method('generate')
|
||||
->willReturn('https://example.test/welcome/[email protected]&nonce=current&expires=9999999999');
|
||||
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$mailer
|
||||
->expects(self::once())
|
||||
->method('createAndSendEmail');
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer, $bpnConnectClient);
|
||||
|
||||
self::assertTrue($manager->provisionGuestAccountForBooking('[email protected]', 123));
|
||||
self::assertInstanceOf(User::class, $persistedUser);
|
||||
self::assertSame('[email protected]', $persistedUser->getEmail());
|
||||
self::assertSame(12345, $persistedUser->getAddressId());
|
||||
self::assertSame(67890, $persistedUser->getPersonId());
|
||||
}
|
||||
|
||||
public function testRequestPortalAccessRejectsMismatchedBookingGenerically(): void
|
||||
{
|
||||
$booking = $this->createLookupResult('[email protected]', 123, 'Alice', 'Example');
|
||||
|
||||
$bpnConnectClient = $this->createMock(BpnConnectClient::class);
|
||||
$bpnConnectClient
|
||||
->expects(self::once())
|
||||
->method('lookupBooking')
|
||||
->with('[email protected]', 123)
|
||||
->willReturn($booking);
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::once())
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn(null);
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager->expects(self::never())->method('persist');
|
||||
$entityManager->expects(self::never())->method('flush');
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$passwordHasher->expects(self::never())->method('hashPassword');
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->expects(self::never())->method('generate');
|
||||
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$mailer->expects(self::never())->method('createAndSendEmail');
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer, $bpnConnectClient);
|
||||
|
||||
self::assertSame(BookingGuestAccountManager::PORTAL_ACCESS_NOT_SENT, $manager->requestPortalAccess('[email protected]', 123));
|
||||
}
|
||||
|
||||
public function testConsumeActivationMarksInviteAsActivated(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
$user->setWelcomeActivationNonce('current-nonce');
|
||||
$user->setWelcomeActivationExpiresAt(new \DateTimeImmutable('+30 minutes'));
|
||||
|
||||
$manager = $this->createValidationManager($user);
|
||||
$manager->consumeActivation($user);
|
||||
|
||||
self::assertNull($user->getWelcomeActivationNonce());
|
||||
self::assertNull($user->getWelcomeActivationExpiresAt());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $user->getWelcomeActivatedAt());
|
||||
}
|
||||
|
||||
public function testValidateActivationRequestAcceptsValidSignedRequestAndRejectsReplay(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
$user->setWelcomeActivationNonce('current-nonce');
|
||||
|
||||
$manager = $this->createValidationManager($user);
|
||||
$request = Request::create(
|
||||
$this->buildActivationUrl('[email protected]', 'current-nonce', time() + 3600),
|
||||
'GET'
|
||||
);
|
||||
|
||||
self::assertSame($user, $manager->validateActivationRequest($request));
|
||||
|
||||
$manager->consumeActivation($user);
|
||||
|
||||
$this->expectException(NotFoundHttpException::class);
|
||||
$manager->validateActivationRequest($request);
|
||||
}
|
||||
|
||||
public function testValidateActivationRequestRejectsExpiredLink(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
$user->setWelcomeActivationNonce('current-nonce');
|
||||
|
||||
$manager = $this->createValidationManager($user);
|
||||
$request = Request::create(
|
||||
$this->buildActivationUrl('[email protected]', 'current-nonce', time() - 60),
|
||||
'GET'
|
||||
);
|
||||
|
||||
$this->expectException(GuestAccountActivationExpiredException::class);
|
||||
$manager->validateActivationRequest($request);
|
||||
}
|
||||
|
||||
public function testValidateActivationUrlForResendAcceptsExpiredLink(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
$user->setWelcomeActivationNonce('current-nonce');
|
||||
|
||||
$manager = $this->createValidationManager($user);
|
||||
$activationUrl = $this->buildActivationUrl('[email protected]', 'current-nonce', time() - 60);
|
||||
|
||||
self::assertSame($user, $manager->validateActivationUrlForResend($activationUrl));
|
||||
}
|
||||
|
||||
public function testResendWelcomeEmailRotatesNonceAndSendsActivationMail(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
$user->setWelcomeActivationNonce('old-nonce');
|
||||
|
||||
$generatedRoute = null;
|
||||
$generatedParameters = null;
|
||||
$generatedReferenceType = null;
|
||||
$capturedContext = null;
|
||||
$capturedOptions = null;
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
|
||||
$entityManager->expects(self::once())->method('flush');
|
||||
$passwordHasher->expects(self::never())->method('hashPassword');
|
||||
$userRepository->expects(self::never())->method('findOneByNormalizedEmail');
|
||||
$urlGenerator
|
||||
->expects(self::once())
|
||||
->method('generate')
|
||||
->willReturnCallback(static function (string $route, array $parameters, int $referenceType) use (&$generatedRoute, &$generatedParameters, &$generatedReferenceType): string {
|
||||
$generatedRoute = $route;
|
||||
$generatedParameters = $parameters;
|
||||
$generatedReferenceType = $referenceType;
|
||||
|
||||
return 'https://example.test/welcome/activate?'.http_build_query($parameters);
|
||||
});
|
||||
$mailer
|
||||
->expects(self::once())
|
||||
->method('createAndSendEmail')
|
||||
->willReturnCallback(static function (array $context, array $options) use (&$capturedContext, &$capturedOptions): void {
|
||||
$capturedContext = $context;
|
||||
$capturedOptions = $options;
|
||||
});
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer);
|
||||
|
||||
$manager->resendWelcomeEmail($user);
|
||||
|
||||
self::assertSame('app_guest_account_activate', $generatedRoute);
|
||||
self::assertSame(UrlGeneratorInterface::ABSOLUTE_URL, $generatedReferenceType);
|
||||
self::assertSame('[email protected]', $generatedParameters['email']);
|
||||
self::assertSame($user->getWelcomeActivationNonce(), $generatedParameters['nonce']);
|
||||
self::assertSame('[email protected]', $capturedOptions['to']);
|
||||
self::assertSame('Dein MyE&P Konto ist bereit', $capturedOptions['subject']);
|
||||
self::assertSame('email/booking_guest_account_welcome.html.twig', $capturedOptions['template']);
|
||||
self::assertTrue((new UriSigner('test-secret'))->check((string) $capturedContext['activationUrl']));
|
||||
self::assertNotSame('old-nonce', $user->getWelcomeActivationNonce());
|
||||
self::assertMatchesRegularExpression('/^[a-f0-9]{64}$/', (string) $user->getWelcomeActivationNonce());
|
||||
}
|
||||
|
||||
public function testProvisionGuestAccountKeepsPersistedUserWhenMailSendFails(): void
|
||||
{
|
||||
$persistedUser = null;
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->expects(self::once())
|
||||
->method('findOneByNormalizedEmail')
|
||||
->with('[email protected]')
|
||||
->willReturn(null);
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager
|
||||
->expects(self::once())
|
||||
->method('persist')
|
||||
->with(self::callback(static function (User $user) use (&$persistedUser): bool {
|
||||
$persistedUser = $user;
|
||||
|
||||
return true;
|
||||
}));
|
||||
$entityManager->expects(self::once())->method('flush');
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$passwordHasher
|
||||
->expects(self::once())
|
||||
->method('hashPassword')
|
||||
->willReturn('hashed-password');
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator
|
||||
->expects(self::once())
|
||||
->method('generate')
|
||||
->willReturn('https://example.test/welcome/[email protected]&nonce=current&expires=9999999999');
|
||||
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$mailer
|
||||
->expects(self::once())
|
||||
->method('createAndSendEmail')
|
||||
->willThrowException(new TransportException('smtp down'));
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer);
|
||||
|
||||
$this->expectException(TransportException::class);
|
||||
try {
|
||||
$manager->provisionGuestAccount('[email protected]', 'Alice', 'Example');
|
||||
} finally {
|
||||
self::assertInstanceOf(User::class, $persistedUser);
|
||||
self::assertSame('[email protected]', $persistedUser->getEmail());
|
||||
self::assertSame(['ROLE_USER', 'ROLE_CUSTOMER'], $persistedUser->getRoles());
|
||||
self::assertTrue($persistedUser->isProfileComplete());
|
||||
self::assertNotNull($persistedUser->getWelcomeActivationNonce());
|
||||
}
|
||||
}
|
||||
|
||||
private function createManager(
|
||||
UserRepository $userRepository,
|
||||
EntityManagerInterface $entityManager,
|
||||
UserPasswordHasherInterface $passwordHasher,
|
||||
UrlGeneratorInterface $urlGenerator,
|
||||
Mailer $mailer,
|
||||
?BpnConnectClient $bpnConnectClient = null,
|
||||
): BookingGuestAccountManager {
|
||||
$bpnConnectClient ??= $this->createMock(BpnConnectClient::class);
|
||||
|
||||
return new BookingGuestAccountManager(
|
||||
$bpnConnectClient,
|
||||
$userRepository,
|
||||
$entityManager,
|
||||
new UserBootstrapper(),
|
||||
$passwordHasher,
|
||||
$urlGenerator,
|
||||
new UriSigner('test-secret'),
|
||||
$mailer,
|
||||
new NullLogger(),
|
||||
);
|
||||
}
|
||||
|
||||
private function createValidationManager(User $user): BookingGuestAccountManager
|
||||
{
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$userRepository
|
||||
->method('findOneByNormalizedEmail')
|
||||
->willReturnCallback(static function (string $email) use ($user): ?User {
|
||||
return '[email protected]' === $email ? $user : null;
|
||||
});
|
||||
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
$bpnConnectClient = $this->createMock(BpnConnectClient::class);
|
||||
|
||||
return $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer, $bpnConnectClient);
|
||||
}
|
||||
|
||||
private function buildActivationUrl(string $email, string $nonce, int $expires): string
|
||||
{
|
||||
$baseUrl = 'https://example.test/welcome/activate?'.http_build_query([
|
||||
'email' => $email,
|
||||
'nonce' => $nonce,
|
||||
'expires' => $expires,
|
||||
]);
|
||||
|
||||
return (new UriSigner('test-secret'))->sign($baseUrl);
|
||||
}
|
||||
|
||||
private function createLookupResult(string $email, int $bookingNumber, string $firstName, string $lastName): BookingLookupResult
|
||||
{
|
||||
return new BookingLookupResult(
|
||||
bookingNumber: $bookingNumber,
|
||||
bookingId: 98765,
|
||||
addressId: 12345,
|
||||
personId: 67890,
|
||||
email: $email,
|
||||
firstName: $firstName,
|
||||
lastName: $lastName,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -173,6 +173,35 @@ class PasswordResetManagerTest extends TestCase
|
||||
self::assertMatchesRegularExpression('/^[a-f0-9]{64}$/', (string) $user->getPasswordResetNonce());
|
||||
}
|
||||
|
||||
public function testResetPasswordClearsPendingWelcomeInvite(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
$user->setPasswordResetNonce('current-nonce');
|
||||
$user->markWelcomeActivationPending('welcome-nonce', new \DateTimeImmutable('-30 minutes'));
|
||||
|
||||
$userRepository = $this->createMock(UserRepository::class);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$passwordHasher = $this->createMock(UserPasswordHasherInterface::class);
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$mailer = $this->createMock(Mailer::class);
|
||||
|
||||
$passwordHasher
|
||||
->expects(self::once())
|
||||
->method('hashPassword')
|
||||
->with($user, 'new-password')
|
||||
->willReturn('hashed-password');
|
||||
$entityManager->expects(self::once())->method('flush');
|
||||
|
||||
$manager = $this->createManager($userRepository, $entityManager, $passwordHasher, $urlGenerator, $mailer);
|
||||
|
||||
$manager->resetPassword($user, 'new-password');
|
||||
|
||||
self::assertSame('hashed-password', $user->getPassword());
|
||||
self::assertNull($user->getWelcomeActivationNonce());
|
||||
self::assertNull($user->getWelcomeActivationExpiresAt());
|
||||
self::assertNotNull($user->getWelcomeActivatedAt());
|
||||
}
|
||||
|
||||
public function testNewPasswordResetRequestInvalidatesPreviousLink(): void
|
||||
{
|
||||
$user = new User('[email protected]');
|
||||
|
||||
@@ -28,6 +28,11 @@ class TestPublicFormRateLimiter extends PublicFormRateLimiter
|
||||
return $this->consume($email, $request);
|
||||
}
|
||||
|
||||
public function isPortalAccessAllowed(string $email, Request $request): bool
|
||||
{
|
||||
return $this->consume($email, $request);
|
||||
}
|
||||
|
||||
public function createBucketKey(string $email, ?string $clientIp): string
|
||||
{
|
||||
return hash('sha256', mb_strtolower(trim($email)).'|'.($clientIp ?? 'unknown'));
|
||||
|
||||
Reference in new issue
Block a user