authenticator( $this->crmAttributes([Role::ADMIN, Role::TEAMER], ['SSL', 'SSL']), null, $persisted, ); $user = $this->loadUser($authenticator); self::assertSame($persisted, $user); self::assertSame(['ROLE_USER', Role::TEAMER, Role::pending(Role::ADMIN)], $user->getRoles()); self::assertSame(['SSL'], $user->getHotelCodes()); } public function testAdministrativeClaimIsOnlyANominationUntilItIsApproved(): void { $existing = (new User('teamer@example.org'))->setRoles([Role::TEAMER]); $persisted = null; $authenticator = $this->authenticator( $this->crmAttributes([Role::TEAMER, Role::GROUPS_ADMIN], []), $existing, $persisted, ); $user = $this->loadUser($authenticator); self::assertSame($existing, $persisted, 'a login must not create a second account'); self::assertSame( ['ROLE_USER', Role::TEAMER, Role::pending(Role::GROUPS_ADMIN)], $user->getRoles(), ); self::assertNotNull($user->getLastLoginAt(), 'the rest of the profile is still synced'); } public function testApprovedRoleSurvivesTheNextLogin(): void { $existing = (new User('manager@example.org'))->setRoles([Role::TEAMER, Role::GROUPS_MANAGER]); $persisted = null; $authenticator = $this->authenticator( $this->crmAttributes([Role::TEAMER, Role::GROUPS_MANAGER], []), $existing, $persisted, ); self::assertSame( ['ROLE_USER', Role::TEAMER, Role::GROUPS_MANAGER], $this->loadUser($authenticator)->getRoles(), ); } public function testRoleRevokedInBusProIsWithdrawnOnLogin(): void { $existing = (new User('manager@example.org'))->setRoles([Role::TEAMER, Role::GROUPS_MANAGER]); $persisted = null; $authenticator = $this->authenticator($this->crmAttributes([], []), $existing, $persisted); // Nothing is claimed any more, so nothing is held — and an account without an effective // role is a customer. self::assertSame(['ROLE_USER', Role::CUSTOMER], $this->loadUser($authenticator)->getRoles()); } public function testRoleGainedInBusProIsGrantedOnLogin(): void { $existing = (new User('teamer@example.org'))->setRoles([Role::CUSTOMER]); $persisted = null; $authenticator = $this->authenticator( $this->crmAttributes([Role::TEAMER], []), $existing, $persisted, ); // The case myep-team depends on: somebody becomes a Teamer after their account exists. self::assertSame(['ROLE_USER', Role::TEAMER], $this->loadUser($authenticator)->getRoles()); } public function testHotelCodesAreResyncedOnEveryLogin(): void { $existing = (new User('house@example.org')) ->setRoles([Role::HOUSE_MANAGER]) ->setHotelCodes(['DKS', 'SSL']) ; $persisted = null; $authenticator = $this->authenticator( $this->crmAttributes([Role::HOUSE_MANAGER], ['DKS']), $existing, $persisted, ); self::assertSame(['DKS'], $this->loadUser($authenticator)->getHotelCodes()); } public function testDegradedCrmResponseLeavesAnExistingAccountUntouched(): void { $existing = (new User('manager@example.org')) ->setRoles([Role::TEAMER, Role::GROUPS_MANAGER]) ->setHotelCodes(['DKS']) ; $persisted = null; // No selection groups at all: BusPro always answers with the full attribute tree, so // this is a degraded payload and not a revocation of everything. $authenticator = $this->authenticator( $this->crmAttributes([], [], selectionGroups: []), $existing, $persisted, ); $user = $this->loadUser($authenticator); self::assertSame(['ROLE_USER', Role::TEAMER, Role::GROUPS_MANAGER], $user->getRoles()); self::assertSame(['DKS'], $user->getHotelCodes()); } public function testDegradedCrmResponseStillGivesANewAccountTheFallbackRole(): void { $persisted = null; $authenticator = $this->authenticator( $this->crmAttributes([], [], selectionGroups: []), null, $persisted, ); self::assertSame(['ROLE_USER', Role::CUSTOMER], $this->loadUser($authenticator)->getRoles()); } public function testANewAccountIsOnlyRegisteredOnceItCarriesItsPassword(): void { $persisted = null; $persistedPassword = null; // A degraded payload is what makes syncFromCrm() log, and that log write reaches the // database. An account registered before the profile is complete would be written out // half-built, and the user table rejects it: password is NOT NULL. $authenticator = $this->authenticator( $this->crmAttributes([], [], selectionGroups: []), null, $persisted, $persistedPassword, ); $this->loadUser($authenticator); self::assertInstanceOf(User::class, $persisted); self::assertSame('encrypted', $persistedPassword); } /** * @param string[] $roles * @param string[] $hotelCodes * @param CrmSelectionGroup[] $selectionGroups only their presence matters here — an empty * set is what marks a response as degraded */ private function crmAttributes(array $roles, array $hotelCodes, ?array $selectionGroups = null): CrmAttributes { $attributes = new CrmAttributes(); $attributes->roles = $roles; $attributes->hotelCodes = $hotelCodes; $attributes->selectionGroups = $selectionGroups ?? [new CrmSelectionGroup()]; return $attributes; } private function authenticator( CrmAttributes $crmAttributes, ?User $existing, ?User &$persisted, ?string &$persistedPassword = null, ): BpnAuthenticator { $personalData = new PersonalData(); $personalData->personId = 42; $personalData->addressId = 4711; $apiClient = $this->createMock(ApiClient::class); $apiClient->method('getPersonalData')->willReturn($personalData); $apiClient->method('getCrmAttributes')->willReturn($crmAttributes); $repository = $this->createMock(EntityRepository::class); $repository->method('findOneBy')->willReturn($existing); $entityManager = $this->createMock(EntityManagerInterface::class); $entityManager->method('getRepository')->willReturn($repository); $entityManager ->method('persist') ->willReturnCallback(static function (object $entity) use (&$persisted, &$persistedPassword): void { $persisted = $entity; // Snapshot rather than a reference: what matters is what the account looked like // at the moment it was registered, not what it grew into afterwards. $persistedPassword = $entity instanceof User ? $entity->getPassword() : null; }) ; $crypt = $this->createMock(Crypt::class); $crypt->method('encrypt')->willReturn('encrypted'); $completenessChecker = $this->createMock(ProfileCompletenessChecker::class); $completenessChecker->method('isComplete')->willReturn(true); return new BpnAuthenticator( $this->createMock(UrlGeneratorInterface::class), $apiClient, $entityManager, $crypt, $completenessChecker, $this->createMock(LoggerInterface::class), ); } private function loadUser(BpnAuthenticator $authenticator): User { $request = new Request(); $request->request->set('_username', 'teamer@example.org'); $request->request->set('_password', 'secret'); $badge = $authenticator->authenticate($request)->getBadge(UserBadge::class); self::assertInstanceOf(UserBadge::class, $badge); $user = $badge->getUser(); self::assertInstanceOf(User::class, $user); return $user; } }