Files
myep-team/tests/BusProNet/UserDataHandlerTest.php
T

498 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\BusProNet;
use App\BusProNet\Model\Address as BusProAddress;
use App\BusProNet\Model\Communication as BusProCommunication;
use App\BusProNet\Model\CrmAttributesResponse;
use App\BusProNet\Model\ProfileResponse;
use App\BusProNet\UserDataHandler;
use App\Entity\Embeddable\Address;
use App\Entity\Embeddable\Communication;
use App\Entity\Teamer;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectRepository;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class UserDataHandlerTest extends TestCase
{
private EntityManagerInterface&MockObject $entityManager;
private LoggerInterface&MockObject $logger;
protected function setUp(): void
{
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
}
/**
* @dataProvider collectRolesProvider
*/
public function testCollectRolesImportsAdministrativeRolesAsPendingOnly(CrmAttributesResponse $crmAttributes, array $expectedRoles): void
{
$handler = new UserDataHandler($this->entityManager, $this->logger);
$this->assertSame($expectedRoles, $handler->collectRoles($crmAttributes));
}
public static function collectRolesProvider(): iterable
{
yield 'admin only yields the pending marker' => [
(new CrmAttributesResponse())->setAdmin(true),
[User::PENDING_ROLES['ROLE_ADMIN']],
];
yield 'manager only yields the pending marker' => [
(new CrmAttributesResponse())->setManager(true),
[User::PENDING_ROLES['ROLE_MANAGER']],
];
yield 'house manager only yields the pending marker' => [
(new CrmAttributesResponse())->setHouseManager(true),
[User::PENDING_ROLES['ROLE_HOUSE_MANAGER']],
];
yield 'teamer is granted directly' => [
(new CrmAttributesResponse())->setTeamer(true),
['ROLE_TEAMER'],
];
yield 'admin and teamer' => [
(new CrmAttributesResponse())->setAdmin(true)->setTeamer(true),
[User::PENDING_ROLES['ROLE_ADMIN'], 'ROLE_TEAMER'],
];
yield 'admin and manager yield both markers' => [
(new CrmAttributesResponse())->setAdmin(true)->setManager(true),
[User::PENDING_ROLES['ROLE_ADMIN'], User::PENDING_ROLES['ROLE_MANAGER']],
];
yield 'manager takes precedence over house manager' => [
(new CrmAttributesResponse())->setManager(true)->setHouseManager(true),
[User::PENDING_ROLES['ROLE_MANAGER']],
];
yield 'house manager and teamer' => [
(new CrmAttributesResponse())->setHouseManager(true)->setTeamer(true),
[User::PENDING_ROLES['ROLE_HOUSE_MANAGER'], 'ROLE_TEAMER'],
];
}
public function testUpdateLocalUserSyncsUserAndTeamerDataFromBusPro(): void
{
$user = (new User())
->setFirstName('Old')
->setLastName('Name')
->setEmail('[email protected]')
->setBusProAddressId(1)
->setBusProPersonId(2)
->setRoles(['ROLE_ADMIN'])
->setHotelCodes(['XYZ'])
;
$teamer = (new Teamer())
->setFirstName('OldTeamer')
->setLastName('OldLastname')
->setAcademicTitle('Dr.')
->setSalutation('Herr')
->setGender('M')
->setDateOfBirth(new \DateTimeImmutable('1990-01-01'))
->setAddress((new Address())->setStreet('Old Street')->setPostCode('11111')->setCity('Old City')->setCountry('DE'))
->setCommunication((new Communication())->setPhone('123')->setMobile('456')->setEmail('[email protected]'))
;
$user->setTeamer($teamer);
$profileResponse = $this->createProfileResponse();
$this->entityManager
->expects($this->once())
->method('flush');
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->updateLocalUser(
$user,
$profileResponse,
true,
['team' => ['selected' => true]],
);
$this->assertSame('New', $user->getFirstName());
$this->assertSame('Lastname', $user->getLastName());
$this->assertSame('[email protected]', $user->getEmail());
// roles and hotel codes are imported on creation only and stay under manual control
$this->assertSame(['XYZ'], $user->getHotelCodes());
$this->assertTrue($user->hasRole('ROLE_ADMIN'));
$this->assertSame('New', $teamer->getFirstName());
$this->assertSame('Lastname', $teamer->getLastName());
$this->assertSame('Prof.', $teamer->getAcademicTitle());
$this->assertSame('Frau', $teamer->getSalutation());
$this->assertSame('F', $teamer->getGender());
$this->assertEquals(new \DateTimeImmutable('1995-12-24'), $teamer->getDateOfBirth());
$this->assertSame('New Street 123', $teamer->getAddress()?->getStreet());
$this->assertSame('54321', $teamer->getAddress()?->getPostCode());
$this->assertSame('New City', $teamer->getAddress()?->getCity());
$this->assertSame('AT', $teamer->getAddress()?->getCountry());
$this->assertSame('999', $teamer->getCommunication()?->getPhone());
$this->assertSame('888', $teamer->getCommunication()?->getMobile());
$this->assertSame('[email protected]', $teamer->getCommunication()?->getEmail());
$this->assertSame(['team' => ['selected' => true]], $teamer->getCrmSelections());
}
/**
* @dataProvider pendingRolesProvider
*/
public function testUpdateLocalUserRefreshesThePendingRoles(
array $roles,
array $claimedRoles,
array $expectedAssignedRoles,
array $expectedPendingRoles,
): void {
$user = (new User())
->setFirstName('Old')
->setLastName('Name')
->setEmail('[email protected]')
->setRoles($roles)
;
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->updateLocalUser($user, $this->createProfileResponse(), false, [], $claimedRoles);
$this->assertSame($expectedAssignedRoles, $user->getAssignedRoles());
$this->assertSame($expectedPendingRoles, $user->getPendingRoles());
}
public static function pendingRolesProvider(): iterable
{
yield 'marker is added when the CRM claims a manager' => [
['ROLE_TEAMER'],
[User::PENDING_ROLES['ROLE_MANAGER']],
['ROLE_TEAMER'],
[User::PENDING_ROLES['ROLE_MANAGER']],
];
yield 'marker is dropped when the CRM attribute is gone' => [
[User::PENDING_ROLES['ROLE_HOUSE_MANAGER'], 'ROLE_TEAMER'],
[],
['ROLE_TEAMER'],
[],
];
yield 'an approved role is never marked again' => [
['ROLE_MANAGER'],
[User::PENDING_ROLES['ROLE_MANAGER']],
['ROLE_MANAGER'],
[],
];
yield 'a claim beyond the approved role stays pending' => [
['ROLE_MANAGER'],
[User::PENDING_ROLES['ROLE_ADMIN'], User::PENDING_ROLES['ROLE_MANAGER']],
['ROLE_MANAGER'],
[User::PENDING_ROLES['ROLE_ADMIN']],
];
yield 'the claimed role changes' => [
[User::PENDING_ROLES['ROLE_HOUSE_MANAGER']],
[User::PENDING_ROLES['ROLE_MANAGER']],
[],
[User::PENDING_ROLES['ROLE_MANAGER']],
];
yield 'granted roles are untouched without any claim' => [
['ROLE_ADMIN'],
[],
['ROLE_ADMIN'],
[],
];
}
public function testUpdateLocalUserGrantsTheTeamerRoleToAUserWhoBecameATeamer(): void
{
// created as a candidate for approval, made a teamer in the CRM afterwards
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setRoles([User::PENDING_ROLES['ROLE_ADMIN']])
;
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->updateLocalUser($user, $this->createProfileResponse(), true, [], [User::PENDING_ROLES['ROLE_ADMIN']]);
$this->assertSame(['ROLE_TEAMER'], $user->getAssignedRoles());
$this->assertSame([User::PENDING_ROLES['ROLE_ADMIN']], $user->getPendingRoles());
}
public function testUpdateLocalUserKeepsTheTeamerRoleOfSomebodyTheCrmNoLongerReportsAsTeamer(): void
{
// the role may have been granted manually and must survive a login
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setRoles(['ROLE_ADMIN', 'ROLE_TEAMER'])
;
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->updateLocalUser($user, $this->createProfileResponse(), false, [], []);
$this->assertSame(['ROLE_ADMIN', 'ROLE_TEAMER'], $user->getAssignedRoles());
}
public function testUpdateLocalUserGrantsTheTeamerRoleOnlyOnce(): void
{
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setRoles(['ROLE_TEAMER'])
;
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->updateLocalUser($user, $this->createProfileResponse(), true, [], []);
$this->assertSame(['ROLE_TEAMER'], $user->getAssignedRoles());
}
public function testDisableForRevokedCrmRolesBlocksTheUserAndDropsThePendingMarkers(): void
{
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setRoles(['ROLE_ADMIN', User::PENDING_ROLES['ROLE_MANAGER']])
;
$this->entityManager
->expects($this->once())
->method('flush');
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->disableForRevokedCrmRoles($user);
$this->assertTrue($user->isDisabled());
$this->assertNotNull($user->getDisabledAt());
$this->assertSame('Für deinen Account liegt in BusPro keine Berechtigung mehr vor.', $user->getDisabledReason());
$this->assertSame('Automatisch gesperrt: keine Rollen in BusPro.', $user->getDisabledReasonInternal());
// the granted role is kept so the user stays reviewable, the marker is not
$this->assertSame(['ROLE_ADMIN'], $user->getAssignedRoles());
$this->assertSame([], $user->getPendingRoles());
}
public function testDisableForRevokedCrmRolesLeavesAnExistingBlockUntouchedButStillFlushes(): void
{
$disabledAt = new \DateTimeImmutable('2026-01-01 08:00:00');
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setRoles(['ROLE_ADMIN'])
->setDisabledAt($disabledAt)
->setDisabledReason('Wegen Fehlverhaltens gesperrt.')
->setDisabledReasonInternal('Siehe Vorgang 4711.')
;
// findLocalUser() may have refreshed the BusPro ids on the way here
$this->entityManager
->expects($this->once())
->method('flush');
$handler = new UserDataHandler($this->entityManager, $this->logger);
$handler->disableForRevokedCrmRoles($user);
$this->assertSame($disabledAt, $user->getDisabledAt());
$this->assertSame('Wegen Fehlverhaltens gesperrt.', $user->getDisabledReason());
$this->assertSame('Siehe Vorgang 4711.', $user->getDisabledReasonInternal());
}
public function testFindLocalUserFallsBackToUniqueEmailAndRefreshesBusProIds(): void
{
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setBusProAddressId(1)
->setBusProPersonId(2)
;
$profileResponse = $this->createProfileResponse();
$repository = $this->createMock(ObjectRepository::class);
$repository
->expects($this->once())
->method('findOneBy')
->with([
'busProAddressId' => 200,
'busProPersonId' => 100,
])
->willReturn(null);
$repository
->expects($this->once())
->method('findBy')
->with([
'email' => '[email protected]',
])
->willReturn([$user]);
$this->entityManager
->expects($this->once())
->method('getRepository')
->with(User::class)
->willReturn($repository);
$handler = new UserDataHandler($this->entityManager, $this->logger);
$resolvedUser = $handler->findLocalUser($profileResponse);
$this->assertSame($user, $resolvedUser);
$this->assertSame(200, $user->getBusProAddressId());
$this->assertSame(100, $user->getBusProPersonId());
}
public function testFindLocalUserSkipsEmailFallbackWhenMultipleUsersExist(): void
{
$userA = (new User())
->setFirstName('A')
->setLastName('A')
->setEmail('[email protected]')
->setBusProAddressId(1)
->setBusProPersonId(2)
;
$userB = (new User())
->setFirstName('B')
->setLastName('B')
->setEmail('[email protected]')
->setBusProAddressId(3)
->setBusProPersonId(4)
;
$profileResponse = $this->createProfileResponse();
$repository = $this->createMock(ObjectRepository::class);
$repository
->expects($this->once())
->method('findOneBy')
->willReturn(null);
$repository
->expects($this->once())
->method('findBy')
->willReturn([$userA, $userB]);
$this->entityManager
->expects($this->once())
->method('getRepository')
->with(User::class)
->willReturn($repository);
$this->logger
->expects($this->once())
->method('warning')
->with(
'Unable to match local user by email: multiple users found',
[
'email' => '[email protected]',
'count' => 2,
],
);
$handler = new UserDataHandler($this->entityManager, $this->logger);
$resolvedUser = $handler->findLocalUser($profileResponse);
$this->assertNull($resolvedUser);
}
public function testFindLocalUserReturnsUniqueEmailMatchWhenBusProIdsAreMissing(): void
{
$user = (new User())
->setFirstName('First')
->setLastName('Last')
->setEmail('[email protected]')
->setBusProAddressId(1)
->setBusProPersonId(2)
;
$profileResponse = $this->createProfileResponse(null, null);
$repository = $this->createMock(ObjectRepository::class);
$repository
->expects($this->once())
->method('findOneBy')
->with([
'busProAddressId' => null,
'busProPersonId' => null,
])
->willReturn(null);
$repository
->expects($this->once())
->method('findBy')
->with([
'email' => '[email protected]',
])
->willReturn([$user]);
$this->entityManager
->expects($this->once())
->method('getRepository')
->with(User::class)
->willReturn($repository);
$this->logger
->expects($this->once())
->method('warning')
->with(
'Skip BusPro ID refresh: profile response missing IDs',
[
'user_id' => null,
'user_email' => '[email protected]',
'bus_pro_address_id' => null,
'bus_pro_person_id' => null,
],
);
$handler = new UserDataHandler($this->entityManager, $this->logger);
$resolvedUser = $handler->findLocalUser($profileResponse);
$this->assertSame($user, $resolvedUser);
$this->assertSame(1, $user->getBusProAddressId());
$this->assertSame(2, $user->getBusProPersonId());
}
private function createProfileResponse(?int $addressId = 200, ?int $personId = 100): ProfileResponse
{
$address = (new BusProAddress())
->setStreet('New Street 123')
->setPostCode('54321')
->setCity('New City')
->setCountry('AT')
;
$communication = (new BusProCommunication())
->setPhone('999')
->setMobile('888')
->setEmail('[email protected]')
;
return (new ProfileResponse())
->setAddressId($addressId)
->setPersonId($personId)
->setFirstName('New')
->setName('Lastname')
->setTitle('Prof.')
->setSalutation('Frau')
->setGender('F')
->setDateOfBirth(new \DateTimeImmutable('1995-12-24'))
->setAddress($address)
->setCommunication($communication)
;
}
}