setAddressId(10) ->setPersonId(20) ->setProfileComplete(false) ; $user->setPassword(password_hash('secret', PASSWORD_BCRYPT)); $passwordHasher = $this->createMock(UserPasswordHasherInterface::class); $passwordHasher ->expects(self::once()) ->method('isPasswordValid') ->with($user, 'secret') ->willReturn(true); $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 = 'customer@example.com'; $personalData->address->street = 'Main Street 1'; $personalData->address->postCode = '10115'; $personalData->address->city = 'Berlin'; $personalData->address->country = 'DE'; $apiClient = $this->createMock(ApiClient::class); $apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', 10, 20) ->willReturn($personalData); $apiClient ->expects(self::once()) ->method('getCrmAttributes') ->with('customer@example.com', 123, 456) ->willReturn((function (): CrmAttributes { $crmAttributes = new CrmAttributes(); $crmAttributes->roles = ['ROLE_ADMIN']; $crmAttributes->hotelCodes = ['AAA']; return $crmAttributes; })()); $authenticator = $this->createAuthenticator( user: $user, apiClient: $apiClient, passwordHasher: $passwordHasher, ); $passport = $authenticator->authenticate($this->createRequest('customer@example.com', 'secret')); self::assertInstanceOf(SelfValidatingPassport::class, $passport); self::assertSame(123, $user->getAddressId()); self::assertSame(456, $user->getPersonId()); self::assertTrue($user->isProfileComplete()); self::assertSame(['ROLE_USER', 'ROLE_ADMIN'], $user->getRoles()); self::assertSame(['AAA'], $user->getHotelCodes()); } 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'); $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 = 'customer@example.com'; $personalData->address->street = 'Main Street 1'; $personalData->address->postCode = '10115'; $personalData->address->city = 'Berlin'; $personalData->address->country = 'DE'; $apiClient = $this->createMock(ApiClient::class); $apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($personalData); $apiClient ->expects(self::once()) ->method('getCrmAttributes') ->with('customer@example.com', 123, 456) ->willReturn((function (): CrmAttributes { $crmAttributes = new CrmAttributes(); $crmAttributes->roles = ['ROLE_CUSTOMER']; $crmAttributes->hotelCodes = []; return $crmAttributes; })()); $authenticator = $this->createAuthenticator( user: $user, apiClient: $apiClient, crypt: $crypt, passwordHasher: $passwordHasher, ); $passport = $authenticator->authenticate($this->createRequest('customer@example.com', 'secret')); self::assertInstanceOf(SelfValidatingPassport::class, $passport); self::assertSame('local-hash', $user->getPassword()); self::assertSame(123, $user->getAddressId()); self::assertSame(456, $user->getPersonId()); } public function testAuthenticateFailsClosedWhenCrmSyncFailsAfterLocalPasswordValidation(): void { $user = new User('customer@example.com'); $user ->setPassword(password_hash('secret', PASSWORD_BCRYPT)) ->setRoles(['ROLE_ADMIN']) ->setHotelCodes(['STALE']) ; $passwordHasher = $this->createMock(UserPasswordHasherInterface::class); $passwordHasher ->expects(self::once()) ->method('isPasswordValid') ->with($user, 'secret') ->willReturn(true); $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 = 'customer@example.com'; $personalData->address->street = 'Main Street 1'; $personalData->address->postCode = '10115'; $personalData->address->city = 'Berlin'; $personalData->address->country = 'DE'; $apiClient = $this->createMock(ApiClient::class); $apiClient ->expects(self::once()) ->method('getPersonalData') ->with('customer@example.com', null, null) ->willReturn($personalData); $apiClient ->expects(self::once()) ->method('getCrmAttributes') ->with('customer@example.com', 123, 456) ->willThrowException(new TimeoutException()); $validator = $this->createMock(ValidatorInterface::class); $validator ->expects(self::once()) ->method('validate') ->with($personalData, null, ['personal_data']) ->willReturn(new ConstraintViolationList()); $authenticator = $this->createAuthenticator( user: $user, apiClient: $apiClient, passwordHasher: $passwordHasher, validator: $validator, expectFlush: false, ); $this->expectException(CustomUserMessageAuthenticationException::class); $authenticator->authenticate($this->createRequest('customer@example.com', 'secret')); } 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'); $validator = $this->createMock(ValidatorInterface::class); $validator ->expects(self::once()) ->method('validate') ->with($personalData, null, ['personal_data']) ->willReturn(new ConstraintViolationList()); $userBootstrapper = new UserBootstrapper(); $persistedUser = null; $repository = $this->createMock(UserRepository::class); $repository ->expects(self::once()) ->method('findOneByNormalizedEmail') ->with($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), $passwordHasher, $validator, $userBootstrapper, $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()); } public function testAuthenticatePreservesNullBpnIdsWhenBootstrappingMissingUser(): void { $email = 'customer@example.com'; $password = 'secret'; $personalData = new PersonalData(); $personalData->addressId = null; $personalData->personId = null; $crmAttributes = new CrmAttributes(); $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'); $validator = $this->createMock(ValidatorInterface::class); $validator ->expects(self::once()) ->method('validate') ->with($personalData, null, ['personal_data']) ->willReturn(new ConstraintViolationList()); $persistedUser = null; $repository = $this->createMock(UserRepository::class); $repository ->expects(self::once()) ->method('findOneByNormalizedEmail') ->with($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), $passwordHasher, $validator, new UserBootstrapper(), $this->createMock(LoggerInterface::class), ); $authenticator->authenticate($this->createRequest($email, $password)); self::assertInstanceOf(User::class, $persistedUser); self::assertNull($persistedUser->getAddressId()); self::assertNull($persistedUser->getPersonId()); } private function createAuthenticator( ?User $user = null, ?ApiClient $apiClient = null, ?Crypt $crypt = null, ?UserPasswordHasherInterface $passwordHasher = null, ?ValidatorInterface $validator = null, ?UserBootstrapper $userBootstrapper = null, bool $expectFlush = true, ): BpnAuthenticator { $validatorProvided = null !== $validator; $passwordHasher ??= $this->createMock(UserPasswordHasherInterface::class); $validator ??= $this->createMock(ValidatorInterface::class); if (!$validatorProvided) { $validator ->method('validate') ->willReturn(new ConstraintViolationList()); } $userBootstrapper ??= new UserBootstrapper(); $repository = $this->createMock(UserRepository::class); $repository ->expects(self::once()) ->method('findOneByNormalizedEmail') ->with(self::callback(static function (string $email): bool { return '' !== trim($email); })) ->willReturn($user); $entityManager = $this->createMock(EntityManagerInterface::class); $entityManager ->method('getRepository') ->with(User::class) ->willReturn($repository); $entityManager ->expects($expectFlush ? self::once() : self::never()) ->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), $passwordHasher, $validator, $userBootstrapper, $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', ]); } }