feat: pending roles from crm to be confirmed by superadmins
This commit is contained in:
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -29,6 +30,59 @@ class UserDataHandlerTest extends TestCase
|
||||
$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())
|
||||
@@ -37,6 +91,8 @@ class UserDataHandlerTest extends TestCase
|
||||
->setEmail('[email protected]')
|
||||
->setBusProAddressId(1)
|
||||
->setBusProPersonId(2)
|
||||
->setRoles(['ROLE_ADMIN'])
|
||||
->setHotelCodes(['XYZ'])
|
||||
;
|
||||
|
||||
$teamer = (new Teamer())
|
||||
@@ -61,17 +117,17 @@ class UserDataHandlerTest extends TestCase
|
||||
$handler->updateLocalUser(
|
||||
$user,
|
||||
$profileResponse,
|
||||
['ROLE_TEAMER'],
|
||||
true,
|
||||
['team' => ['selected' => true]],
|
||||
['ABC'],
|
||||
);
|
||||
|
||||
$this->assertSame('New', $user->getFirstName());
|
||||
$this->assertSame('Lastname', $user->getLastName());
|
||||
$this->assertSame('[email protected]', $user->getEmail());
|
||||
$this->assertSame(['ABC'], $user->getHotelCodes());
|
||||
$this->assertTrue($user->hasRole('ROLE_TEAMER'));
|
||||
|
||||
// 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());
|
||||
@@ -89,6 +145,74 @@ class UserDataHandlerTest extends TestCase
|
||||
$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 testFindLocalUserFallsBackToUniqueEmailAndRefreshesBusProIds(): void
|
||||
{
|
||||
$user = (new User())
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Config;
|
||||
|
||||
use App\Config\HouseCatalog;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class HouseCatalogTest extends TestCase
|
||||
{
|
||||
private const HOUSES = [
|
||||
'SVS' => 'Silvana',
|
||||
'AGR' => 'Rotbach',
|
||||
'DPW' => 'Waldschlössli',
|
||||
];
|
||||
|
||||
public function testNameChoicesAreSortedAndMapLabelToName(): void
|
||||
{
|
||||
$catalog = new HouseCatalog(self::HOUSES);
|
||||
|
||||
$this->assertSame([
|
||||
'Rotbach' => 'Rotbach',
|
||||
'Silvana' => 'Silvana',
|
||||
'Waldschlössli' => 'Waldschlössli',
|
||||
], $catalog->getNameChoices());
|
||||
}
|
||||
|
||||
public function testCodeChoicesAreSortedByLabelAndMapLabelToCode(): void
|
||||
{
|
||||
$catalog = new HouseCatalog(self::HOUSES);
|
||||
|
||||
$this->assertSame([
|
||||
'Rotbach' => 'AGR',
|
||||
'Silvana' => 'SVS',
|
||||
'Waldschlössli' => 'DPW',
|
||||
], $catalog->getCodeChoices());
|
||||
}
|
||||
|
||||
public function testGetName(): void
|
||||
{
|
||||
$catalog = new HouseCatalog(self::HOUSES);
|
||||
|
||||
$this->assertSame('Silvana', $catalog->getName('SVS'));
|
||||
$this->assertNull($catalog->getName('XYZ'));
|
||||
}
|
||||
|
||||
public function testEmptyCatalog(): void
|
||||
{
|
||||
$catalog = new HouseCatalog([]);
|
||||
|
||||
$this->assertSame([], $catalog->getNameChoices());
|
||||
$this->assertSame([], $catalog->getCodeChoices());
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
use Symfony\Component\Validator\Validation;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
@@ -23,4 +25,64 @@ class UserTest extends TestCase
|
||||
$isMatch = $user->hasHotelCodeMatch('XXDEFXX');
|
||||
$this->assertFalse($isMatch);
|
||||
}
|
||||
|
||||
public function testPendingMarkersAreLabelledButNotAssignable(): void
|
||||
{
|
||||
$user = (new User())->setRoles([
|
||||
User::PENDING_ROLES['ROLE_ADMIN'],
|
||||
User::PENDING_ROLES['ROLE_HOUSE_MANAGER'],
|
||||
'ROLE_TEAMER',
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
['Admin (nicht freigeschaltet)', 'Hausleitung (nicht freigeschaltet)', 'Teamer'],
|
||||
$user->getRolesLabels(),
|
||||
);
|
||||
$this->assertSame(['ROLE_TEAMER'], $user->getAssignedRoles());
|
||||
$this->assertSame(
|
||||
[User::PENDING_ROLES['ROLE_ADMIN'], User::PENDING_ROLES['ROLE_HOUSE_MANAGER']],
|
||||
$user->getPendingRoles(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testSuperAdminRequiresRoleAdmin(): void
|
||||
{
|
||||
$user = (new User())
|
||||
->setRoles(['ROLE_MANAGER'])
|
||||
->setSuperAdmin(true)
|
||||
;
|
||||
|
||||
$violations = $this->validate($user);
|
||||
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('superAdmin', $violations[0]->getPropertyPath());
|
||||
}
|
||||
|
||||
public function testSuperAdminWithRoleAdminIsValid(): void
|
||||
{
|
||||
$user = (new User())
|
||||
->setRoles(['ROLE_ADMIN'])
|
||||
->setSuperAdmin(true)
|
||||
;
|
||||
|
||||
$this->assertCount(0, $this->validate($user));
|
||||
}
|
||||
|
||||
public function testNonSuperAdminWithoutRoleAdminIsValid(): void
|
||||
{
|
||||
$user = (new User())
|
||||
->setRoles(['ROLE_TEAMER'])
|
||||
;
|
||||
|
||||
$this->assertCount(0, $this->validate($user));
|
||||
}
|
||||
|
||||
private function validate(User $user): ConstraintViolationListInterface
|
||||
{
|
||||
return Validation::createValidatorBuilder()
|
||||
->enableAttributeMapping()
|
||||
->getValidator()
|
||||
->validate($user)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\UserType;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
|
||||
class UserTypeTest extends KernelTestCase
|
||||
{
|
||||
public function testRendersRolesWithoutSynthesizedRoles(): void
|
||||
{
|
||||
$user = (new User())
|
||||
->setRoles(['ROLE_ADMIN'])
|
||||
->setSuperAdmin(true)
|
||||
;
|
||||
|
||||
$view = $this->createForm($user)->createView();
|
||||
|
||||
// ROLE_USER and ROLE_SUPER_ADMIN are synthesized by getRoles() and must not leak in
|
||||
$this->assertSame(['ROLE_ADMIN'], $view->children['roles']->vars['data']);
|
||||
}
|
||||
|
||||
public function testApprovingAPendingRoleClearsTheMarker(): void
|
||||
{
|
||||
$user = (new User())->setRoles([User::PENDING_ROLES['ROLE_ADMIN']]);
|
||||
|
||||
$form = $this->createForm($user);
|
||||
|
||||
// the marker is not an assignable choice and must not reach the field
|
||||
$this->assertSame([], $form->createView()->children['roles']->vars['data']);
|
||||
|
||||
$form->submit([
|
||||
'roles' => ['ROLE_ADMIN'],
|
||||
'superAdmin' => null,
|
||||
'hotelCodes' => [],
|
||||
]);
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
$this->assertSame(['ROLE_ADMIN'], $user->getAssignedRoles());
|
||||
$this->assertSame([], $user->getPendingRoles());
|
||||
}
|
||||
|
||||
public function testRendersHotelCodesNotCoveredByTheConfiguredMap(): void
|
||||
{
|
||||
$user = (new User())->setHotelCodes(['XYZ']);
|
||||
|
||||
$view = $this->createForm($user)->createView();
|
||||
|
||||
$this->assertSame(['XYZ'], $view->children['hotelCodes']->vars['data']);
|
||||
}
|
||||
|
||||
public function testSubmitStoresAssignedRolesOnly(): void
|
||||
{
|
||||
$user = (new User())->setRoles(['ROLE_TEAMER']);
|
||||
|
||||
$form = $this->createForm($user);
|
||||
$form->submit([
|
||||
'roles' => ['ROLE_ADMIN'],
|
||||
'superAdmin' => '1',
|
||||
'hotelCodes' => [],
|
||||
]);
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
$this->assertSame(['ROLE_ADMIN'], $user->getAssignedRoles());
|
||||
$this->assertTrue($user->isSuperAdmin());
|
||||
}
|
||||
|
||||
public function testSubmitRejectsSuperAdminWithoutRoleAdmin(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
$form = $this->createForm($user);
|
||||
$form->submit([
|
||||
'roles' => ['ROLE_MANAGER'],
|
||||
'superAdmin' => '1',
|
||||
'hotelCodes' => [],
|
||||
]);
|
||||
|
||||
$this->assertFalse($form->isValid());
|
||||
$this->assertCount(1, $form->get('superAdmin')->getErrors());
|
||||
}
|
||||
|
||||
private function createForm(User $user): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
/** @var FormFactoryInterface $formFactory */
|
||||
$formFactory = self::getContainer()->get(FormFactoryInterface::class);
|
||||
|
||||
return $formFactory->create(UserType::class, $user, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Security\Voter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\Voter\UserVoter;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
class UserVoterTest extends TestCase
|
||||
{
|
||||
private Security&MockObject $security;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->security = $this->createMock(Security::class);
|
||||
}
|
||||
|
||||
public function testSuperAdminMayEditOtherUsers(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
VoterInterface::ACCESS_GRANTED,
|
||||
$this->vote($this->createUser(true), $this->createUser(false)),
|
||||
);
|
||||
}
|
||||
|
||||
public function testPlainAdminMayNotEdit(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
VoterInterface::ACCESS_DENIED,
|
||||
$this->vote($this->createUser(false), $this->createUser(false)),
|
||||
);
|
||||
}
|
||||
|
||||
public function testSuperAdminMayNotEditThemselves(): void
|
||||
{
|
||||
$currentUser = $this->createUser(true);
|
||||
|
||||
$this->assertSame(
|
||||
VoterInterface::ACCESS_DENIED,
|
||||
$this->vote($currentUser, $currentUser),
|
||||
);
|
||||
}
|
||||
|
||||
public function testImpersonatorMayNotEdit(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
VoterInterface::ACCESS_DENIED,
|
||||
$this->vote($this->createUser(true), $this->createUser(false), true),
|
||||
);
|
||||
}
|
||||
|
||||
private function vote(User $currentUser, User $targetUser, bool $isImpersonator = false): int
|
||||
{
|
||||
$this->security
|
||||
->method('isGranted')
|
||||
->with('IS_IMPERSONATOR')
|
||||
->willReturn($isImpersonator);
|
||||
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
$token
|
||||
->method('getUser')
|
||||
->willReturn($currentUser);
|
||||
|
||||
$voter = new UserVoter($this->security);
|
||||
|
||||
return $voter->vote($token, $targetUser, [UserVoter::EDIT]);
|
||||
}
|
||||
|
||||
private function createUser(bool $superAdmin): User
|
||||
{
|
||||
return (new User())
|
||||
->setRoles(['ROLE_ADMIN'])
|
||||
->setSuperAdmin($superAdmin)
|
||||
;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user