feat: admin-managed roles and hotel codes
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form\Admin;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Admin\UserType;
|
||||
use App\Security\Role;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\Forms;
|
||||
|
||||
class UserTypeTest extends TestCase
|
||||
{
|
||||
public function testStoredRolesArePreselectedWithoutTheImplicitRoleUser(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))->setRoles([Role::TEAMER]);
|
||||
|
||||
self::assertSame([Role::TEAMER], $this->createForm($user)->get('roles')->getData());
|
||||
}
|
||||
|
||||
public function testSubmittingRolesDoesNotStoreTheImplicitRoleUser(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))->setRoles([Role::TEAMER]);
|
||||
|
||||
$form = $this->createForm($user);
|
||||
$form->submit(['roles' => [Role::TEAMER, Role::GROUPS_MANAGER], 'hotelCodes' => []]);
|
||||
|
||||
self::assertTrue($form->isSynchronized());
|
||||
// getRoles() prepends ROLE_USER; it must not have been persisted a second time.
|
||||
self::assertSame(['ROLE_USER', Role::TEAMER, Role::GROUPS_MANAGER], $user->getRoles());
|
||||
}
|
||||
|
||||
public function testClearingEveryCheckboxEmptiesTheAssignment(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))
|
||||
->setRoles([Role::TEAMER])
|
||||
->setHotelCodes(['SSL'])
|
||||
;
|
||||
|
||||
$form = $this->createForm($user);
|
||||
$form->submit([]);
|
||||
|
||||
self::assertTrue($form->isSynchronized());
|
||||
self::assertSame(['ROLE_USER'], $user->getRoles());
|
||||
self::assertSame([], $user->getHotelCodes());
|
||||
}
|
||||
|
||||
public function testHotelCodeMissingFromTheCatalogSurvivesAnEdit(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))->setHotelCodes(['SSL', 'XYZ']);
|
||||
|
||||
$form = $this->createForm($user);
|
||||
$form->submit(['roles' => [], 'hotelCodes' => ['SSL', 'XYZ']]);
|
||||
|
||||
self::assertTrue($form->isSynchronized());
|
||||
self::assertSame(['SSL', 'XYZ'], $user->getHotelCodes());
|
||||
}
|
||||
|
||||
public function testHotelCodesAreOfferedAlphabetically(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))->setHotelCodes(['ASB']);
|
||||
|
||||
$choices = $this->createForm($user)->get('hotelCodes')->getConfig()->getOption('choices');
|
||||
|
||||
self::assertSame(['ASB', 'DKS', 'SSL'], array_values($choices));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface<User>
|
||||
*/
|
||||
private function createForm(User $user): FormInterface
|
||||
{
|
||||
return Forms::createFormFactoryBuilder()
|
||||
->addType(new UserType(['SSL' => 'SSL', 'DKS' => 'DKS']))
|
||||
->getFormFactory()
|
||||
->create(UserType::class, $user)
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form\Model\Filter;
|
||||
|
||||
use App\Form\Model\Filter\UserFilterDto;
|
||||
use App\Security\Role;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserFilterDtoTest extends TestCase
|
||||
{
|
||||
public function testTheRoleChipShowsTheLabelRatherThanTheRoleName(): void
|
||||
{
|
||||
$filter = new UserFilterDto();
|
||||
$filter->role = Role::GROUPS_ADMIN;
|
||||
|
||||
$chip = $filter->activeFilters()[0];
|
||||
|
||||
self::assertSame('Rolle', $chip->label);
|
||||
self::assertSame('Preisrechner Admin', $chip->value);
|
||||
self::assertSame(['role'], $chip->removeKeys);
|
||||
}
|
||||
|
||||
public function testAnUnknownRoleIsShownVerbatim(): void
|
||||
{
|
||||
$filter = new UserFilterDto();
|
||||
$filter->role = 'ROLE_LEGACY';
|
||||
|
||||
self::assertSame('ROLE_LEGACY', $filter->activeFilters()[0]->value);
|
||||
}
|
||||
|
||||
public function testNoRoleMeansNoChip(): void
|
||||
{
|
||||
self::assertSame([], (new UserFilterDto())->activeFilters());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Security;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\Entity\User;
|
||||
use App\Security\BpnAuthenticator;
|
||||
use App\Security\Crypt;
|
||||
use App\Security\Role;
|
||||
use App\Service\ProfileCompletenessChecker;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
|
||||
/**
|
||||
* Covers who may grant roles: BusPro backend users can edit their own CRM selections, so the
|
||||
* import must not be a channel for privilege escalation.
|
||||
*/
|
||||
class BpnAuthenticatorTest extends TestCase
|
||||
{
|
||||
public function testNewAccountIsSeededWithTheImportableRolesOnly(): void
|
||||
{
|
||||
$persisted = null;
|
||||
$authenticator = $this->authenticator(
|
||||
$this->crmAttributes([Role::ADMIN, Role::TEAMER, Role::GROUPS_ADMIN], ['SSL', 'SSL']),
|
||||
null,
|
||||
$persisted,
|
||||
);
|
||||
|
||||
$user = $this->loadUser($authenticator);
|
||||
|
||||
self::assertSame($persisted, $user);
|
||||
self::assertSame(['ROLE_USER', Role::TEAMER], $user->getRoles());
|
||||
self::assertSame(['SSL'], $user->getHotelCodes());
|
||||
}
|
||||
|
||||
public function testExistingAccountKeepsTheRolesAnAdministratorAssigned(): void
|
||||
{
|
||||
$existing = (new User('[email protected]'))
|
||||
->setRoles([Role::TEAMER, Role::GROUPS_MANAGER])
|
||||
->setHotelCodes(['DKS'])
|
||||
;
|
||||
|
||||
$persisted = null;
|
||||
$authenticator = $this->authenticator(
|
||||
$this->crmAttributes([Role::ADMIN, Role::CUSTOMER], ['SSL']),
|
||||
$existing,
|
||||
$persisted,
|
||||
);
|
||||
|
||||
$user = $this->loadUser($authenticator);
|
||||
|
||||
self::assertNull($persisted, 'an existing account must not be persisted again');
|
||||
self::assertSame(['ROLE_USER', Role::TEAMER, Role::GROUPS_MANAGER], $user->getRoles());
|
||||
self::assertSame(['DKS'], $user->getHotelCodes());
|
||||
self::assertNotNull($user->getLastLoginAt(), 'the rest of the profile is still synced');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $roles
|
||||
* @param string[] $hotelCodes
|
||||
*/
|
||||
private function crmAttributes(array $roles, array $hotelCodes): CrmAttributes
|
||||
{
|
||||
$attributes = new CrmAttributes();
|
||||
$attributes->roles = $roles;
|
||||
$attributes->hotelCodes = $hotelCodes;
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
private function authenticator(CrmAttributes $crmAttributes, ?User $existing, ?User &$persisted): 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): void {
|
||||
$persisted = $entity;
|
||||
})
|
||||
;
|
||||
|
||||
$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', '[email protected]');
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Security;
|
||||
|
||||
use App\Security\Role;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RoleTest extends TestCase
|
||||
{
|
||||
public function testPrivilegedRolesAreNeverImported(): void
|
||||
{
|
||||
$roles = Role::filterImportable([
|
||||
Role::TEAMER,
|
||||
Role::ADMIN,
|
||||
Role::GROUPS_ADMIN,
|
||||
Role::GROUPS_MANAGER,
|
||||
Role::HOUSE_MANAGER,
|
||||
]);
|
||||
|
||||
self::assertSame([Role::TEAMER, Role::HOUSE_MANAGER], $roles);
|
||||
}
|
||||
|
||||
public function testResultIsADedupedList(): void
|
||||
{
|
||||
// CrmAttributesResponseParser applies array_unique(), which preserves keys — a
|
||||
// non-list would be persisted as a JSON object instead of an array.
|
||||
$roles = Role::filterImportable([0 => Role::ADMIN, 2 => Role::TEAMER, 5 => Role::TEAMER]);
|
||||
|
||||
self::assertSame([Role::TEAMER], $roles);
|
||||
self::assertSame(array_keys($roles), range(0, \count($roles) - 1));
|
||||
}
|
||||
|
||||
public function testAccountWithOnlyPrivilegedRolesFallsBackToCustomer(): void
|
||||
{
|
||||
self::assertSame([Role::CUSTOMER], Role::filterImportable([Role::ADMIN]));
|
||||
self::assertSame([Role::CUSTOMER], Role::filterImportable([]));
|
||||
}
|
||||
|
||||
public function testEveryRoleHasALabel(): void
|
||||
{
|
||||
self::assertSame(Role::ALL, array_keys(Role::labels()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Twig;
|
||||
|
||||
use App\Security\Role;
|
||||
use App\Twig\AppRuntime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AppRuntimeMapRolesTest extends TestCase
|
||||
{
|
||||
public function testStoredRolesAreLabelledLikeInTheEditForm(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['Teamer:in', 'Preisrechner'],
|
||||
$this->runtime()->mapRoles([Role::TEAMER, Role::GROUPS_MANAGER]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testImplicitRoleUserIsNotListed(): void
|
||||
{
|
||||
self::assertSame(['Administration'], $this->runtime()->mapRoles(['ROLE_USER', Role::ADMIN]));
|
||||
self::assertSame([], $this->runtime()->mapRoles(['ROLE_USER']));
|
||||
}
|
||||
|
||||
public function testUnknownRoleStaysVisible(): void
|
||||
{
|
||||
self::assertSame(['ROLE_LEGACY'], $this->runtime()->mapRoles(['ROLE_LEGACY']));
|
||||
}
|
||||
|
||||
private function runtime(): AppRuntime
|
||||
{
|
||||
return (new \ReflectionClass(AppRuntime::class))->newInstanceWithoutConstructor();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user