createController(); $controller->apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($this->createPersonalData('Mia', 'Muster')); $controller->newsletterManager ->expects(self::once()) ->method('hasConfirmedOptIn') ->with('customer@example.com') ->willReturn(false); $controller->newsletterManager ->expects(self::once()) ->method('hasPendingConfirmation') ->with('customer@example.com') ->willReturn(false); $response = $controller->index(Request::create('/personal-data', 'GET')); self::assertSame(200, $response->getStatusCode()); self::assertSame('account/personal_data.html.twig', $controller->renderedView); self::assertSame(false, $controller->renderedParameters['newsletterSubscribed']); self::assertSame(false, $controller->renderedParameters['newsletterPendingConfirmation']); } public function testIndexStillTracksPendingNewsletterConfirmationWhenSubscribed(): void { $controller = $this->createController(); $controller->apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($this->createPersonalData('Mia', 'Muster')); $controller->newsletterManager ->expects(self::once()) ->method('hasConfirmedOptIn') ->with('customer@example.com') ->willReturn(true); $controller->newsletterManager ->expects(self::once()) ->method('hasPendingConfirmation') ->with('customer@example.com') ->willReturn(false); $response = $controller->index(Request::create('/personal-data', 'GET')); self::assertSame(200, $response->getStatusCode()); self::assertSame(true, $controller->renderedParameters['newsletterSubscribed']); self::assertSame(false, $controller->renderedParameters['newsletterPendingConfirmation']); } public function testNewsletterActionReturnsHtmxRedirectAndFlash(): void { $controller = $this->createController(); $controller->apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($this->createPersonalData('Mia', 'Muster')); $controller->newsletterManager ->expects(self::once()) ->method('hasPendingConfirmation') ->with('customer@example.com') ->willReturn(false); $controller->newsletterManager ->expects(self::once()) ->method('requestDefaultListSubscription') ->with('customer@example.com', 'Mia', 'Muster') ->willReturn(new NewsletterSubscriptionRequestResult( 'customer@example.com', [10321569], NewsletterSubscriptionRequestResult::STATE_CONFIRMATION_REQUESTED, true, [ 10321569 => NewsletterSubscriptionRequestResult::LIST_STATE_PENDING, ], )); $response = $controller->newsletter(Request::create('/personal-data/newsletter', 'POST', [], [], [], [ 'HTTP_HX_REQUEST' => 'true', ])); self::assertInstanceOf(Response::class, $response); self::assertSame(200, $response->getStatusCode()); self::assertSame('/personal-data', $response->headers->get('HX-Redirect')); self::assertSame([ ['success', 'Bitte bestätige deine Newsletter-Anmeldung über den Link in der E-Mail.'], ], $controller->flashes); } public function testNewsletterActionResendsPendingConfirmation(): void { $controller = $this->createController(); $controller->apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($this->createPersonalData('Mia', 'Muster')); $controller->newsletterManager ->expects(self::once()) ->method('hasPendingConfirmation') ->with('customer@example.com') ->willReturn(true); $controller->newsletterManager ->expects(self::once()) ->method('requestConfirmation') ->with('customer@example.com', 'Mia', 'Muster'); $controller->newsletterManager->expects(self::never())->method('requestDefaultListSubscription'); $response = $controller->newsletter(Request::create('/personal-data/newsletter', 'POST', [], [], [], [ 'HTTP_HX_REQUEST' => 'true', ])); self::assertInstanceOf(Response::class, $response); self::assertSame(200, $response->getStatusCode()); self::assertSame([ ['success', 'Wir haben dir eine neue Bestätigungs-E-Mail gesendet.'], ], $controller->flashes); } public function testIndexRecomputesProfileCompletionAfterSavingPersonalData(): void { $personalData = $this->createCompletePersonalData(); $controller = $this->createController(true, true, $this->validator()); $controller->apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($personalData); $controller->apiClient ->expects(self::once()) ->method('updatePersonalData') ->with('customer@example.com', $personalData) ->willReturn($personalData); $controller->newsletterManager ->expects(self::once()) ->method('hasConfirmedOptIn') ->with('customer@example.com') ->willReturn(false); $controller->newsletterManager ->expects(self::once()) ->method('hasPendingConfirmation') ->with('customer@example.com') ->willReturn(false); $controller->dataLoader ->expects(self::once()) ->method('invalidateUserBookingCaches') ->with($controller->user); $controller->entityManager ->expects(self::once()) ->method('flush'); $controller->passwordHasher->expects(self::never())->method('hashPassword'); $request = Request::create('/personal-data', 'POST'); $request->setSession(new Session(new MockArraySessionStorage())); $response = $controller->index($request); self::assertSame(302, $response->getStatusCode()); self::assertTrue($controller->user->isProfileComplete()); self::assertSame('secret', $controller->user->getPassword()); self::assertSame('legacy-backup', $controller->user->getPreviousPassword()); self::assertSame([ ['success', 'Deine persönlichen Daten wurden aktualisiert'], ], $controller->flashes); } public function testIndexHashesPasswordWhenNewPasswordWasProvided(): void { $personalData = $this->createCompletePersonalData(); $controller = $this->createController(true, true, $this->validator(), 'NewPassw0rd!'); $controller->apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($personalData); $controller->apiClient ->expects(self::once()) ->method('updatePersonalData') ->with('customer@example.com', $personalData) ->willReturn($personalData); $controller->newsletterManager ->expects(self::once()) ->method('hasConfirmedOptIn') ->with('customer@example.com') ->willReturn(false); $controller->newsletterManager ->expects(self::once()) ->method('hasPendingConfirmation') ->with('customer@example.com') ->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())); $response = $controller->index($request); self::assertSame(302, $response->getStatusCode()); self::assertTrue($controller->user->isProfileComplete()); self::assertSame('new-local-hash', $controller->user->getPassword()); self::assertSame('legacy-backup', $controller->user->getPreviousPassword()); self::assertSame([ ['success', 'Deine persönlichen Daten wurden aktualisiert'], ], $controller->flashes); } public function testIndexDoesNotPersistInvalidFormSubmission(): void { $controller = $this->createController(true, false, $this->validator(), 'NewPassw0rd!'); $controller->apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($this->createCompletePersonalData()); $controller->apiClient->expects(self::never())->method('updatePersonalData'); $controller->newsletterManager ->expects(self::once()) ->method('hasConfirmedOptIn') ->with('customer@example.com') ->willReturn(false); $controller->newsletterManager ->expects(self::once()) ->method('hasPendingConfirmation') ->with('customer@example.com') ->willReturn(false); $controller->dataLoader->expects(self::never())->method('invalidateUserBookingCaches'); $controller->entityManager->expects(self::never())->method('flush'); $controller->passwordHasher->expects(self::never())->method('hashPassword'); $request = Request::create('/personal-data', 'POST'); $request->setSession(new Session(new MockArraySessionStorage())); $response = $controller->index($request); self::assertSame(200, $response->getStatusCode()); self::assertSame('account/personal_data.html.twig', $controller->renderedView); self::assertSame([], $controller->flashes); self::assertSame('secret', $controller->user->getPassword()); self::assertSame('legacy-backup', $controller->user->getPreviousPassword()); } private function createController( bool $submitted = false, bool $valid = false, ?ValidatorInterface $validator = null, string $newPassword = '', ): TestablePersonalDataController { $user = new User('customer@example.com'); $user->setPassword('secret'); $user->setPreviousPassword('legacy-backup'); $apiClient = $this->createMock(ApiClient::class); $form = $this->createMock(FormInterface::class); $form ->method('handleRequest') ->willReturnSelf(); $form ->method('isSubmitted') ->willReturn($submitted); $form ->method('isValid') ->willReturn($valid); $newPasswordField = $this->createMock(FormInterface::class); $newPasswordField ->method('getData') ->willReturn($newPassword); $form ->method('get') ->with('newPassword') ->willReturn($newPasswordField); $validator ??= $this->createMock(ValidatorInterface::class); return new TestablePersonalDataController( $apiClient, $this->createMock(BookingEditDataLoader::class), $validator, $this->createMock(EntityManagerInterface::class), $this->createMock(NewsletterManager::class), $this->createMock(UserPasswordHasherInterface::class), $this->createMock(LoggerInterface::class), $user, $form, ); } private function createPersonalData(string $firstName, string $lastName): PersonalData { $personalData = new PersonalData(); $personalData->firstName = $firstName; $personalData->name = $lastName; return $personalData; } private function createCompletePersonalData(): PersonalData { $personalData = new PersonalData(); $personalData->firstName = 'Mia'; $personalData->name = 'Muster'; $personalData->gender = 'W'; $personalData->nationality = 'DE'; $personalData->dateOfBirth = new \DateTimeImmutable('1990-05-15'); $personalData->address->street = 'Main Street 1'; $personalData->address->postCode = '10115'; $personalData->address->city = 'Berlin'; $personalData->address->country = 'DE'; $personalData->communication->email = 'customer@example.com'; $personalData->communication->mobile = '0123456789'; return $personalData; } private function validator(): ValidatorInterface { return Validation::createValidatorBuilder() ->enableAttributeMapping() ->setConstraintValidatorFactory(new class extends ConstraintValidatorFactory { public function getInstance(Constraint $constraint): ConstraintValidatorInterface { if (true === ($constraint instanceof Email)) { return new EmailValidator(Email::VALIDATION_MODE_HTML5); } return parent::getInstance($constraint); } }) ->getValidator(); } } final class TestablePersonalDataController extends PersonalDataController { /** * @var array */ public array $flashes = []; /** * @var array */ public array $renderedParameters = []; public string $renderedView = ''; public function __construct( public readonly ApiClient $apiClient, public readonly BookingEditDataLoader $dataLoader, public readonly ValidatorInterface $validator, public readonly EntityManagerInterface $entityManager, public readonly NewsletterManager $newsletterManager, public readonly UserPasswordHasherInterface $passwordHasher, LoggerInterface $logger, public readonly User $user, private readonly FormInterface $form, ) { parent::__construct( $apiClient, $dataLoader, $validator, $entityManager, $newsletterManager, $passwordHasher, $logger, ); } public function createForm(string $type, mixed $data = null, array $options = []): FormInterface { return $this->form; } protected function getUser(): UserInterface { return $this->user; } protected function addFlash(string $type, mixed $message): void { $this->flashes[] = [$type, (string) $message]; } protected function render(string $view, array $parameters = [], Response $response = null): Response { $this->renderedView = $view; $this->renderedParameters = $parameters; return new Response('ok'); } protected function generateUrl(string $route, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string { return '/personal-data'; } }