setPassword(password_hash('secret', PASSWORD_BCRYPT)); $passwordHasher = $this->createMock(UserPasswordHasherInterface::class); $passwordHasher ->expects(self::once()) ->method('isPasswordValid') ->with($user, 'secret') ->willReturn(true); $authenticator = $this->createAuthenticator( user: $user, passwordHasher: $passwordHasher, ); $passport = $authenticator->authenticate($this->createRequest('customer@example.com', 'secret')); self::assertInstanceOf(SelfValidatingPassport::class, $passport); } public function testAuthenticateMigratesLegacyPasswordToLocalHashOnSuccess(): void { $user = new User('customer@example.com'); $user->setPassword(base64_encode('legacy-ciphertext')); $crypt = $this->createMock(Crypt::class); $crypt ->expects(self::once()) ->method('decrypt') ->with('legacy-ciphertext') ->willReturn(md5('secret')); $passwordHasher = $this->createMock(UserPasswordHasherInterface::class); $passwordHasher ->expects(self::never()) ->method('isPasswordValid'); $passwordHasher ->expects(self::once()) ->method('hashPassword') ->with($user, 'secret') ->willReturn('local-hash'); $authenticator = $this->createAuthenticator( user: $user, crypt: $crypt, passwordHasher: $passwordHasher, ); $passport = $authenticator->authenticate($this->createRequest('customer@example.com', 'secret')); self::assertInstanceOf(SelfValidatingPassport::class, $passport); self::assertSame('local-hash', $user->getPassword()); } public function testAuthenticateBootstrapsMissingUserFromBpn(): void { $email = 'customer@example.com'; $password = 'secret'; $personalData = new PersonalData(); $personalData->addressId = 123; $personalData->personId = 456; $personalData->firstName = 'Mia'; $personalData->name = 'Muster'; $personalData->gender = 'W'; $personalData->nationality = 'DE'; $personalData->dateOfBirth = new \DateTimeImmutable('1990-05-15'); $personalData->communication->email = $email; $personalData->communication->mobile = '+49123456789'; $personalData->address->street = 'Main Street 1'; $personalData->address->postCode = '10115'; $personalData->address->city = 'Berlin'; $personalData->address->country = 'DE'; $crmAttributes = new CrmAttributes(); $crmAttributes->roles = ['ROLE_CUSTOMER']; $crmAttributes->hotelCodes = ['AAA']; $apiClient = $this->createMock(ApiClient::class); $apiClient->expects(self::once()) ->method('getPersonalDataWithPassword') ->with($email, $password) ->willReturn($personalData); $apiClient->expects(self::once()) ->method('getCrmAttributesWithPassword') ->with($email, $password) ->willReturn($crmAttributes); $passwordHasher = $this->createMock(UserPasswordHasherInterface::class); $passwordHasher ->expects(self::once()) ->method('hashPassword') ->with(self::isInstanceOf(User::class), $password) ->willReturn('local-hash'); $completenessChecker = $this->createMock(ProfileCompletenessChecker::class); $completenessChecker ->expects(self::once()) ->method('isComplete') ->with($personalData) ->willReturn(true); $persistedUser = null; $repository = $this->createMock(EntityRepository::class); $repository ->expects(self::once()) ->method('findOneBy') ->with(['email' => $email]) ->willReturn(null); $entityManager = $this->createMock(EntityManagerInterface::class); $entityManager ->method('getRepository') ->willReturn($repository); $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'); $authenticator = new BpnAuthenticator( $this->createMock(UrlGeneratorInterface::class), $apiClient, $entityManager, $this->createMock(Crypt::class), $completenessChecker, $passwordHasher, $this->createMock(LoggerInterface::class), ); $passport = $authenticator->authenticate($this->createRequest($email, $password)); self::assertInstanceOf(SelfValidatingPassport::class, $passport); self::assertInstanceOf(User::class, $persistedUser); self::assertSame('local-hash', $persistedUser->getPassword()); self::assertSame(123, $persistedUser->getAddressId()); self::assertSame(456, $persistedUser->getPersonId()); self::assertSame(['ROLE_USER', 'ROLE_CUSTOMER'], $persistedUser->getRoles()); self::assertSame(['AAA'], $persistedUser->getHotelCodes()); self::assertTrue($persistedUser->isProfileComplete()); } private function createAuthenticator( ?User $user = null, ?ApiClient $apiClient = null, ?Crypt $crypt = null, ?ProfileCompletenessChecker $completenessChecker = null, ?UserPasswordHasherInterface $passwordHasher = null, ): BpnAuthenticator { $repository = $this->createMock(EntityRepository::class); $repository ->expects(self::once()) ->method('findOneBy') ->with(self::callback(static function (array $criteria): bool { return isset($criteria['email']) && is_string($criteria['email']); })) ->willReturn($user); $entityManager = $this->createMock(EntityManagerInterface::class); $entityManager ->method('getRepository') ->with(User::class) ->willReturn($repository); $entityManager ->expects(self::once()) ->method('flush'); $entityManager ->expects(null === $user ? self::once() : self::never()) ->method('persist'); return new BpnAuthenticator( $this->createMock(UrlGeneratorInterface::class), $apiClient ?? $this->createMock(ApiClient::class), $entityManager, $crypt ?? $this->createMock(Crypt::class), $completenessChecker ?? $this->createMock(ProfileCompletenessChecker::class), $passwordHasher ?? $this->createMock(UserPasswordHasherInterface::class), $this->createMock(LoggerInterface::class), ); } private function createRequest(string $email, string $password): Request { return Request::create('/login', Request::METHOD_POST, [ '_username' => $email, '_password' => $password, '_csrf_token' => 'csrf-token', ]); } }